diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3ff5676dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,115 @@ +root = true + +[*] +charset = utf-8-bom +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.{cs,csx}] +indent_size = 4 + +# Microsoft-style layout and using directives. +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Prefer modern C# for new and touched code without forcing a noisy legacy rewrite. +csharp_style_namespace_declarations = file_scoped:silent +csharp_prefer_braces = true:suggestion +csharp_prefer_static_local_function = true:silent +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_switch_expression = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:silent +csharp_style_pattern_matching_over_as_with_null_check = true:silent +csharp_style_inlined_variable_declaration = true:silent +csharp_style_throw_expression = true:silent +csharp_style_conditional_delegate_call = true:silent +csharp_prefer_simple_default_expression = true:silent + +# Use C# keywords and var only when the type is obvious from the expression. +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = false:silent + +# Prefer concise, maintainable expressions where semantics stay clear. +dotnet_style_object_initializer = true:silent +dotnet_style_collection_initializer = true:silent +dotnet_style_prefer_collection_expression = when_types_exactly_match:silent +dotnet_style_coalesce_expression = true:silent +dotnet_style_null_propagation = true:silent +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:silent +dotnet_style_readonly_field = true:silent + +# Avoid unnecessary qualification. +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# Modifier order follows the current .NET runtime convention. +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:silent + +# Spacing. +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Generated sources are not hand-maintained style targets. +[**/*.Designer.cs] +generated_code = true + +[*.{csproj,props,targets,resx,config,nuspec}] +indent_size = 2 + +[*.{json,yml,yaml}] +indent_size = 2 + +[*.ps1] +indent_size = 2 + +[*.sh] +charset = utf-8 +end_of_line = lf +indent_size = 2 + +[.gitattributes] +charset = utf-8 +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes index f5c970bbf..a4182f38b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,15 @@ -* text=auto +* text=auto eol=crlf -# geosite database -*.dat binary \ No newline at end of file +# Binary files +*.ico binary +*.png binary +*.pdn binary +*.dat binary +*.dll binary +*.exe binary +*.sys binary +*.zip binary +*.gz binary + +# Unix scripts must keep a valid shebang +*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report_en.md b/.github/ISSUE_TEMPLATE/bug_report_en.md index d1998ede0..afa5852a0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_en.md +++ b/.github/ISSUE_TEMPLATE/bug_report_en.md @@ -1,39 +1,50 @@ ---- +--- name: Bug report (English) -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - +about: Report a reproducible Shadowsocks Reborn problem +title: "" +labels: bug +assignees: "" --- - + -### Describe the bug +## Problem -### Environment +Describe the problem and what you expected to happen. -- Shadowsocks client version: -- OS version: -- .NET version: +## Environment -### Steps you have tried +- Shadowsocks Reborn version/commit: +- Windows version/build: +- Installation: release ZIP / source build +- .NET 10 SDK version (source builds only): +- Traffic mode: User / Admin +- System proxy mode: Disabled / PAC / Global +- Launch location: local disk / read-only folder / removable drive +## Steps to reproduce -### What did you expect to see? +1. +2. +3. +## Admin Mode details -### What did you see instead? + +- Did UAC succeed: yes / no +- WinDivert status on the Traffic page: active / paused / inactive +- Does the log contain `WinDivert capture confirmed`: yes / no +- Was a Game Mode application running: yes / no -### Config and error log in detail (with all sensitive info masked) +## Logs +Paste the complete relevant WARN/ERROR and stack trace. + +```text +PASTE SANITIZED LOG HERE ``` -PASTE LOG HERE -``` + +## Additional context + +Add screenshots or minimal configuration fragments when useful. Remove private data first. diff --git a/.github/ISSUE_TEMPLATE/bug_report_ru.md b/.github/ISSUE_TEMPLATE/bug_report_ru.md new file mode 100644 index 000000000..d333315b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_ru.md @@ -0,0 +1,50 @@ +--- +name: Сообщение об ошибке (Русский) +about: Сообщить о воспроизводимой проблеме Shadowsocks Reborn +title: "" +labels: bug +assignees: "" +--- + + + +## Проблема + +Опиши, что происходит и какой результат ожидался. + +## Окружение + +- Версия/commit Shadowsocks Reborn: +- Версия/build Windows: +- Запуск: release ZIP / сборка из исходников +- Версия .NET 10 SDK (только для сборки из исходников): +- Режим трафика: User / Admin +- Системный proxy: Disabled / PAC / Global +- Место запуска: локальный диск / read-only каталог / флешка + +## Шаги воспроизведения + +1. +2. +3. + +## Admin Mode + + + +- UAC прошёл успешно: да / нет +- Статус WinDivert на странице Traffic: активен / приостановлен / неактивен +- Есть ли в логе `WinDivert capture confirmed`: да / нет +- Было ли запущено приложение из списка Game Mode: да / нет + +## Логи + +Приложи полный относящийся к ошибке WARN/ERROR и stack trace. + +```text +ВСТАВЬ ОЧИЩЕННЫЙ ЛОГ +``` + +## Дополнительно + +При необходимости добавь скриншоты или минимальный фрагмент конфига. Сначала удали приватные данные. diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh.md b/.github/ISSUE_TEMPLATE/bug_report_zh.md index 17d46f707..47ab2ec2d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_zh.md +++ b/.github/ISSUE_TEMPLATE/bug_report_zh.md @@ -1,39 +1,46 @@ +--- +name: Bug 报告(中文) +about: 反馈可复现的 Shadowsocks Reborn 客户端问题 +title: "" +labels: bug +assignees: "" --- -name: Bug报告 (中文) -about: 反馈Bug -title: '' -labels: bug report -assignees: '' ---- + - +## 问题 -### 简要描述问题 +请描述实际结果和预期结果。 -### 环境 +## 环境 -- Shadowsocks客户端版本: -- 操作系统版本: -- .NET版本: +- Shadowsocks Reborn 版本/commit: +- Windows 版本/build: +- 安装方式:release ZIP / 源码构建 +- .NET 10 SDK 版本(仅源码构建): +- 流量模式:User / Admin +- 系统代理模式:Disabled / PAC / Global +- 启动位置:本地磁盘 / 只读目录 / 可移动磁盘 -### 操作步骤 +## 复现步骤 +1. +2. +3. -### 期望的结果 +## Admin Mode + -### 实际结果 +- UAC 是否成功:是 / 否 +- Traffic 页面中的 WinDivert 状态:active / paused / inactive +- 日志中是否有 `WinDivert capture confirmed`:是 / 否 +- 是否正在运行 Game Mode 列表中的应用:是 / 否 +## 日志 -### 配置文件和日志文件(请隐去敏感信息) +请粘贴完整且已脱敏的 WARN/ERROR 和 stack trace。 -``` +```text 在此粘贴日志 ``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 24473dee7..1ee571452 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,32 @@ ---- +--- name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - +about: Suggest a focused improvement to Shadowsocks Reborn +title: "" +labels: enhancement +assignees: "" --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +## Problem + +What concrete limitation or use case should be addressed? + +## Proposed behavior + +Describe the desired user-visible result. + +## Technical impact + +If known, note any impact on: -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +- User Mode / Admin Mode / automatic Game Mode; +- WinDivert or elevated helper behavior; +- PAC / GeoSite / system proxy; +- Registry settings and current LocalAppData/Clean Mode storage behavior; +- Shadowsocks protocol or SIP003 behavior; +- x64 release packaging; +- WinUI 3 UI and localization; +- single-file release / LocalAppData / Temp storage policy. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +## Alternatives -**Additional context** -Add any other context or screenshots about the feature request here. \ No newline at end of file +Describe other approaches you considered. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 88187a169..0f789a9aa 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,21 +1,20 @@ -## Please follow the guide below +## Summary -- You will be asked some questions, please read them **carefully** and answer honestly -- Put an `x` into all the boxes [ ] relevant to your *pull request* (like that [x]) -- Use *Preview* tab to see how your *pull request* will actually look like +Describe what changed and why. -- [ ] [Searched](https://github.com/shadowsocks/shadowsocks-windows/search?q=is%3Apr&type=Issues) for similar pull requests -- [ ] Compiled the code with Visual Studio -- [ ] Require translation update -- [ ] Require document update (readme.md, wikipage, etc) +## Validation -### What is the purpose of your *pull request*? -- [ ] Bug fix -- [ ] Improvement -- [ ] New feature +- [ ] Built the solution with .NET 10 for x64. +- [ ] Ran relevant tests. +- [ ] Ran `packaging/Build-Release.ps1` when build/publish/runtime packaging changed. +- [ ] Runtime-tested the changed path. +- [ ] Tested PAC/Global persistence if system-proxy code changed. +- [ ] Tested Admin Mode and automatic Game Mode if WinDivert/routing code changed. +- [ ] Opened and runtime-tested affected WinUI pages/views. +- [ ] Added/updated localization for user-visible strings. +- [ ] Updated the relevant README/CHANGELOG/architecture/security/storage/UI/release-checklist Markdown when behavior, requirements or validator invariants changed. +- [ ] Confirmed no passwords, server addresses, tokens or private configs are included. ---- +## Notes -### Description of your *pull request* and other information - -Explanation of your *pull request* in arbitrary form goes here. Please make sure the description explains the purpose and effect of your *pull request* and is worded well enough to be understood. Provide as much context and examples as possible. \ No newline at end of file +Include exact commands, runtime scenarios, screenshots and known limitations when relevant. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..60251401f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: CI + +on: + push: + branches: [main, v4] + pull_request: + branches: [main, v4] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-test-publish: + runs-on: windows-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate repository resources + shell: pwsh + run: .\packaging\Validate-Repository.ps1 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: .NET info + run: dotnet --info + + - name: Restore Core standalone + run: dotnet restore .\Shadowsocks.Core\Shadowsocks.Core.csproj -p:NuGetAudit=true -p:NuGetAuditMode=all + + - name: Build Core standalone + run: dotnet build .\Shadowsocks.Core\Shadowsocks.Core.csproj -c Release --no-restore -p:TreatWarningsAsErrors=true + + - name: Restore + run: dotnet restore .\shadowsocks-reborn.sln -p:Platform=x64 -r win-x64 -p:NuGetAudit=true -p:NuGetAuditMode=all + + - name: Build + run: dotnet build .\shadowsocks-reborn.sln -c Release -p:Platform=x64 -m:1 --no-restore -p:TreatWarningsAsErrors=true + + - name: Test + run: dotnet test .\Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj -c Release -p:Platform=x64 --no-build + + - name: Restore publish runtime + run: dotnet restore .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 -p:NuGetAudit=true -p:NuGetAuditMode=all + + - name: Publish + run: dotnet publish .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -c Release -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 --self-contained true -o .\artifacts\publish --no-restore -p:TreatWarningsAsErrors=true + + - name: Validate publish + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $publish = Join-Path $PWD 'artifacts\publish' + $publishedExecutable = Join-Path $publish 'Shadowsocks.exe' + if (-not (Test-Path -LiteralPath $publishedExecutable -PathType Leaf)) { + throw "Missing required publish artifact: Shadowsocks.exe" + } + + $entries = @(Get-ChildItem -LiteralPath $publish -Force) + if ($entries.Count -ne 1 -or $entries[0].PSIsContainer -or $entries[0].Name -ne 'Shadowsocks.exe') { + $entryNames = @($entries | ForEach-Object { $_.Name }) -join ', ' + throw "Final product publish must contain exactly Shadowsocks.exe. Found: $entryNames" + } + + # Directory.Build.props is the canonical product-version source. Individual + # projects intentionally inherit and must not duplicate it. + [xml]$versionProps = Get-Content -LiteralPath .\Directory.Build.props -Raw + $versionNode = $versionProps.SelectSingleNode('/Project/PropertyGroup/Version') + if ($null -eq $versionNode) { + throw 'Directory.Build.props does not declare .' + } + + $declaredVersionText = ([string]$versionNode.InnerText).Trim() + if ($declaredVersionText -notmatch '^\d+\.\d+\.\d+$') { + throw "Directory.Build.props Version '$declaredVersionText' must be a stable three-part version." + } + + $declaredVersion = [Version]$declaredVersionText + $expectedFileVersion = [Version]::new($declaredVersion.Major, $declaredVersion.Minor, $declaredVersion.Build, 0) + $versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($publishedExecutable) + $actualFileVersion = [Version]::new( + [Math]::Max(0, $versionInfo.FileMajorPart), + [Math]::Max(0, $versionInfo.FileMinorPart), + [Math]::Max(0, $versionInfo.FileBuildPart), + [Math]::Max(0, $versionInfo.FilePrivatePart)) + if ($actualFileVersion -ne $expectedFileVersion) { + throw "Published Shadowsocks.exe FileVersion '$actualFileVersion' does not match '$expectedFileVersion'." + } + + foreach ($name in @( + 'Shadowsocks.NetworkService.exe', + 'Shadowsocks.NetworkService.dll', + 'Shadowsocks.NetworkService.deps.json', + 'Shadowsocks.NetworkService.runtimeconfig.json', + 'Shadowsocks.pdb', + 'privoxy.exe', + 'sysproxy.exe', + 'sysproxy64.exe', + 'libsscrypto.dll', + 'dnscrypt-proxy.exe', + 'minisign.exe', + 'libsodium.dll', + 'libsodium-23.dll', + 'gui-config.json')) { + if (Test-Path -LiteralPath (Join-Path $publish $name)) { + throw "Forbidden publish sidecar found: $name" + } + } + + - name: Upload publish artifact + uses: actions/upload-artifact@v7 + with: + name: shadowsocks-reborn-win-x64 + path: artifacts/publish/ + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..669b813d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: Package version label, for example v5.2.31 + required: true + default: v5.2.31 + +permissions: + contents: write + +jobs: + package: + runs-on: windows-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Resolve version label + id: version + shell: pwsh + run: | + if ('${{ github.event_name }}' -eq 'workflow_dispatch') { + "label=${{ inputs.version }}" >> $env:GITHUB_OUTPUT + } + else { + "label=${{ github.ref_name }}" >> $env:GITHUB_OUTPUT + } + + - name: Build release package + shell: pwsh + env: + RELEASE_VERSION: ${{ steps.version.outputs.label }} + run: .\packaging\Build-Release.ps1 -Version $env:RELEASE_VERSION + + - name: Upload package artifact + uses: actions/upload-artifact@v7 + with: + name: shadowsocks-reborn-${{ steps.version.outputs.label }}-win-x64 + path: | + artifacts/release/*.zip + artifacts/release/*.sha256 + if-no-files-found: error + + - name: Create GitHub Release + if: github.event_name == 'push' + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $zip = Get-ChildItem .\artifacts\release\*.zip | Select-Object -First 1 + $hash = Get-ChildItem .\artifacts\release\*.sha256 | Select-Object -First 1 + gh release create $env:GITHUB_REF_NAME $zip.FullName $hash.FullName --title "shadowsocks-reborn $env:GITHUB_REF_NAME" --generate-notes --verify-tag --draft diff --git a/.gitignore b/.gitignore index 61dc74987..58eb0dcfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -## Ignore Visual Studio and VSCode temporary files, build results, and +## Ignore Visual Studio and VSCode temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore @@ -23,7 +23,6 @@ mono_crash.* [Rr]elease/ [Rr]eleases/ x64/ -x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ @@ -364,3 +363,13 @@ MigrationBackup/ !.vscode/launch.json !.vscode/extensions.json *.code-workspace + +# Runtime PAC/GeoSite caches +/dlc.dat +/online-pac-cache.pac +/online-pac-cache.meta.json + +/geosite-cache/ + +# Generated by Fody for XML IntelliSense; not part of the source tree. +FodyWeavers.xsd diff --git a/CHANGES b/CHANGES index 3773974b7..d74d8701f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,110 @@ +5.2.31.0 2026-08-23 +- Fixed CI publish-version validation to use `Directory.Build.props` as the single canonical version source instead of incorrectly requiring a project-local `` in `Shadowsocks.WinUI.csproj`. GitHub workflows are now shipped in the release-ready source archive and repository validation enforces this version-source contract, exact single-file publish layout and StrictMode-safe diagnostic formatting. +- Fixed the repository localization gate to match runtime `StringComparer.Ordinal` semantics: exact duplicate English keys remain forbidden, while intentional case-distinct keys such as `Active` / `ACTIVE` are valid. Canonicalized stale case-only UI keys for GeoSite/System Proxy/Traffic Mode and removed unused lower-case duplicates; added localization regression coverage. +- Fixed a PowerShell runtime-semantic regression in the repository validator: helper functions no longer use `return if (...)`, which PowerShell interprets as an attempt to invoke a command named `if`. Project/package property helpers now use statement-style `if` blocks with explicit returns, and the repository gate rejects direct `return` operands that are PowerShell statement keywords. +- Fixed the PowerShell release validator under `Set-StrictMode -Version Latest`: success-path checks with empty collections no longer dereference synthetic `.FullName` / `.Name` properties while formatting diagnostics. Repository, DNSCrypt and release-package scripts now project collection member names explicitly before joining, and the repository gate rejects the unsafe pattern going forward. +- Fixed DNSCrypt first-use default regressions exposed by the full test suite: active runtime proxy routing still defaults ON and still requires a valid local SOCKS5 port, while lifecycle/TOML fixtures that intentionally model direct DNSCrypt now opt out explicitly. Resolver-catalog TOML is always offline/cache-only and no longer inherits the active runtime proxy flag. +- Zero-server gating now preserves the saved DNS policy instead of rewriting it to System. With no usable server, Windows system proxy, DNSCrypt runtime and Administrator/WinDivert capture remain effectively inactive; adding a configured server reapplies the saved preference through the normal reload path. +- Cleared the final CA1859 release analyzer warning in online-config selection recovery by using the concrete List type already passed by Configuration.configs; behavior is unchanged. +- Fixed the Servers page WinUI compile regression introduced by zero-server UX gating: Border is a visual container and has no IsEnabled property, so editor availability is now applied to the actual TextBox/NumberBox/PasswordBox/ComboBox/CheckBox controls. The repository validator now rejects IsEnabled assignments to known non-Control WinUI container/visual types. +- Final routing hardening excludes incomplete legacy/damaged server profiles from Balancing/High Availability, tray selection and server hotkeys; High Availability and SIP003 plugin process maps now keep same-endpoint profiles distinct by reference identity. TCP strategy connections use the timeout of the server actually selected by the strategy, and subscription refresh preserves the selected profile without colliding with a local server at the same host/port. +- Final release audit fixed two state-integrity defects: persisted DNSCrypt compatibility fields are no longer rewritten while loading/applying visible DNSCrypt settings (the effective Administrator-mode DNSCrypt capture remains fail-closed), and saving a mixed local/subscription server list now preserves the actually selected server when grouping reorders the list. +- Corrected the built-in QTun catalog Windows asset selector: upstream v0.2.0 names the release ZIP with the `x86_64-pc-windows-msvc` target triple, while `windows-native` is only the GitHub Actions artifact label. Added a catalog-install regression test using the upstream archive convention. +- Fixed balancing-strategy UDP selection for endpoints whose hash code is negative; unsigned modulo now guarantees a valid server index instead of allowing a negative list index. Added a deterministic regression test. +- Fixed empty-server first-run UX: the historical synthetic `Server 1` placeholder is removed from new and migrated configurations, zero servers is a valid state, and an incomplete draft no longer counts as a usable Shadowsocks server. +- Server-dependent features are now gated consistently: DNS, PAC and enabling the Windows system proxy are unavailable until at least one complete server configuration exists; removing the last usable server disables effective proxy/DNS/capture activity while preserving the saved DNS policy for later reuse. +- Added responsive busy feedback for operations that can block on file/registry/UAC/update work: Start on Boot now performs its heavy work off the UI thread with a ProgressRing, traffic-capture/UAC mode changes show progress, and About update actions expose an explicit busy state. +- New DNSCrypt configurations now default every first-use option on except IPv6 (`Auto update`, `DNSSEC`, `No log`, `No filter`, `IPv4`, `Route through Shadowsocks`, `Automatic resolvers`, `Fail closed`). Persisted DNSCrypt settings are never rewritten by the schema-v3 migration; only genuinely new DNSCrypt configuration objects receive these defaults. +- Replaced WinUI navigation/status/UAC glyphs that depended on Windows 11-era Segoe Fluent Icons with explicit Windows 10-compatible Segoe MDL2 Assets glyphs, including Logs and the Administrator/UAC shield. +- Fixed DNSCrypt resolver-catalog installation against the current upstream Minisign format: the catalog verifier now accepts the official legacy `Ed` form over the original file bytes in addition to modern pre-hashed `ED` over BLAKE2b-512, while retaining key-id, payload-signature and trusted-comment global-signature verification. Legacy acceptance is scoped to resolver catalogs; downloaded dnscrypt-proxy release archives remain strict modern-`ED` only. +- Self-update handoff now resumes the live shell state captured at update time instead of reusing the process's original startup argument, so opening or hiding the window after launch is preserved across an automatic/manual application update. +- Fixed reboot/session restore state preservation: Start with Windows and Restart Manager remain mutually exclusive but now track the actual shell state with `--start-visible` / `--start-hidden`; `WM_QUERYENDSESSION` snapshots the state before Windows closes the HWND, so an open window returns open while a tray-only session stays in the tray. Both restore markers are also excluded from plain second-launch handling, preventing redirected system restore from showing the already-running dialog. +- Fixed release validation drift: the DNSCrypt StrictMode validator now initializes its controller source path, and the repository validator checks the current `FilePath` / `FallbackFilePath` logging schema instead of the retired `FileName` field. +- Routed DNSCrypt component GitHub release discovery and archive/signature downloads through the same Cloudflare-then-Google DoH-over-Shadowsocks transport used by resolver-catalog bootstrap; GitHub redirect hosts are re-resolved through DoH, component network failures are logged before rollback, and WinUI now reports a localized tunneled-upstream error instead of a raw system-DNS host-not-found message. +- Cleared the final DNSCrypt TOML CA1859 diagnostics without weakening analyzer rules: private static-resolver serialization and its test fixture now expose the concrete Dictionary type they already materialize. +- Fixed DNSCrypt resolver-catalog bootstrap deadlock: resolver-list hostnames are now resolved with Cloudflare DoH over the active local Shadowsocks SOCKS5 tunnel, with Google DoH as fallback; the catalog and `.minisig` are fetched through the same tunnel, verified with the pinned DNSCrypt Minisign key, then exposed to dnscrypt-proxy only as a local authenticated cache with `urls = []`, `bootstrap_resolvers = []` and `ignore_system_dns = true`. A valid stale signed cache remains usable if refresh is temporarily unavailable. +- Completed a full WinUI localization/accessibility/context-state audit: the selected app locale now owns the built-in Settings navigation item, indirect Hotkey labels and registration-status tooltips; missing Forward Proxy labels are localized across all seven shipped locales; scenario-only options are hidden instead of left as misleading disabled controls; and action buttons now follow real selection/dirty/busy state across Servers, PAC/GeoSite, Online Config, Plugins, Games, Hotkeys, Sharing, Traffic and Updates. +- Tightened contextual UI safety: invalid persisted Online PAC URLs are corrected before activation from either page or tray, GeoSite refresh shows busy progress and blocks conflicting edits, duplicate Add actions are disabled, update installation/checking locks conflicting update controls, selecting Plugin=None hides plugin-specific fields without destroying unsaved values until Save, and the obsolete editor for the generated Local PAC file was removed in favor of user-rule.txt. +- Improved first-use UX around routing components: importing an `ss://` URL now detects a missing built-in SIP003 plugin and offers to install it before server import; first DNSCrypt activation keeps an animated operation card visible while installation/verification is running; the PAC / GeoSite page exposes `user-rule.txt` editing; and Local managed-routing diagnostics/user-rule/GeoSite cards are hidden while Online PAC is selected. +- Fixed stale embedded-legal-resource tests after the managed ABP migration: assertions now use the current `Adblock Plus-derived network routing` notice heading instead of the retired PAC-helper wording. +- Made plugin release test fixtures byte-stable: each fake GitHub release now creates its ZIP payload once and reuses the exact bytes for both the advertised SHA-256 digest and the download response, preventing ZIP timestamp metadata from causing nondeterministic digest failures before anti-downgrade checks. +- Fixed the WinUI DNS resolver-selection compile regression left by the cancellation-ownership refactor: the visibility handler now cancels latency probing through the current `_cancelResolverLatency` delegate instead of referencing the removed `_resolverLatencyCancellation` field. +- Completed the WinUI analyzer/lifetime follow-up: App, DNS and Logs now have explicit shutdown ownership; cached disposable pages are released before the controller; DNS background cancellation sources are owned/disposed by their async operations; Logs uses a non-disposable .NET Lock for serialized settings writes; and the remaining CA1725/CA1822/CA1834/CA1305/CA1806/CA1859 diagnostics were corrected with concrete types, invariant formatting and retained application lifetime. +- Fixed the strict-build follow-up diagnostics reported by Visual Studio: TrafficPage local-editor variables now use consistent identifiers, private hot-path signatures use their concrete types, traffic queue validation uses the .NET 10 throw helper, test APM bridges use non-generic TaskCompletionSource, process-environment formatting/pointer-length checks are culture/64-bit safe, and security-pin tests inspect runtime constant metadata instead of compile-time-folded assertions. Test-only CA1707/CA1861 are scoped off because Method_Scenario_Result names and local literal fixtures are intentional test readability choices; production analyzers remain enabled. +- Added managed background SIP003 catalog updates: trusted built-in packages are checked once per day, active/in-use plugins are deferred, failed network checks remain retryable, per-plugin automatic updates can be disabled, and the Plugins page exposes an explicit Check updates action. +- Hardened plugin package updates with release provenance, optional GitHub SHA-256 asset-digest verification, bounded downloads/extraction, path/Windows-ADS rejection, version-aware anti-downgrade gating, staging plus rollback-safe replacement, interrupted-swap backup recovery, and stale transient-directory cleanup; manual ZIP/TAR.GZ imports never participate in background updates. +- Replaced the non-terminating traffic-statistics Thread and externally mutable Queue with a cancellation-aware Task and locked snapshot API, removing a normal-operation race from the WinUI logs/traffic path. +- Added the arbitrary SIP003 repository field to the project TODO pending a dedicated trust, validation and release-asset-selection design instead of exposing an unsafe free-form updater source. +- Completed the follow-up Windows analyzer pass: fixed relay/controller rename regressions, explicit disposable ownership for proxy/DNSCrypt/WinDivert lifetimes, CancellationToken ordering/propagation, and remaining performance/culture diagnostics without broad analyzer suppression. +- Fixed the follow-up strict .NET 10 build diagnostics from the r29 Windows compile: restored the canonical Configuration.Enabled JSON-compatible property, converted persisted public fields to properties, completed nullable annotations for the shared FilterEngine, modernized AEAD/buffer naming and exception contracts, and applied cancellation/performance analyzer guidance in NetworkService. +- Fixed release-blocking .NET 10 analyzer/compiler findings reported after the r28 review: FlowState tests now pass the fallback route, Listener has explicit socket ownership/disposal, service shutdown no longer exposes a cross-language reserved member, PAC hashes use SHA-256, public routing-rule fields became JSON-compatible properties, and null/culture/dictionary hot paths follow current analyzer guidance. +- Fixed High Availability server reload so removed servers cannot remain in the runtime selection set; server scoring/selection now avoids a temporary list and redundant pass. +- Rebuilt the GitHub Wiki as language-first EN/RU/ZH branches: Home and the global sidebar contain only language navigation, while architecture, release notes, changelog, storage, WinUI, contributing, release checklist, security, license and third-party pages exist per language. +- Release build and publish now pass `TreatWarningsAsErrors=true` so compiler/analyzer regressions block packaging. +- Finalized Variant B managed routing: Local GeoSite + EasyList/ABP network decisions are now exclusively owned by the dependency-free C# FilterEngine in both User and Administrator paths. Removed the historical Local PAC backend selector, C#-compiled PAC compatibility backend, user abp.txt executable-JavaScript override and all related watchers/UI/tests. Local PAC is now only the minimal Windows FindProxyForURL funnel into ManagedHttpProxyService; removed configuration properties are pruned from existing JSON by schema migration. Online PAC remains a separate explicit external-PAC mode. + +- Centralized the three-part product version in Directory.Build.props; release tooling now validates only materialized runtime/manifests/CHANGES metadata against that source instead of duplicating across projects. +- Reworked repository and DNSCrypt PowerShell validation into compact structural/supply-chain gates: parser validity, project/package/publish contracts, localization completeness, encoding, legal resources and pinned runtime provenance remain checked, while behavioral contracts are owned by unit/integration tests rather than source-text snapshots. +- Completed Wiki language parity with full English, Russian and Simplified Chinese user guides, corrected the Shadowsocks.Windows.WinUI project graph, and synchronized all three release-facing README translations. +- Standardized Shadowsocks.Windows.WinUI shell namespaces and enabled the .NET 10 recommended analyzer ruleset in build configuration. +- Fixed About > License and Third-party notices so explanatory legal content follows the selected UI language (EN/RU/zh-CN/zh-TW/JA/KO/FR) while authoritative original license terms remain embedded verbatim. +- Corrected the release version after the previous RC was mistakenly numbered 2.2.31: the product, helper, manifests, packaging defaults, tests and documentation now consistently use 5.2.31 / 5.2.31.0; ordinary System.Version ordering therefore rejects the older 5.2.22 release. +- Fixed stale Start-with-Windows update version detection: update eligibility now uses the higher of the running copy version and the primary EXE FileVersion, while updater mode rechecks the target FileVersion immediately before replacement and refuses equal-version or downgrade payloads. +- Locked automatic-update version selection to strict numeric ordering: an installed 5.2.31 rejects 5.2.22/5.2.31 and accepts only releases greater than 5.2.31; regression tests cover both downgrade/equal rejection and 5.2.32 acceptance. +- Restored synchronized English/Russian/Simplified-Chinese README files to the application repository root while keeping their Wiki mirrors, and corrected stale DNS documentation: current Administrator Mode implements transparent UDP/TCP 53 routing for System/Direct/Proxy/CustomDoh/DnsCrypt; the older pre-5.2.31 limitation is historical only. +- Moved long-form Markdown documentation into the dedicated GitHub Wiki while retaining the three localized root README files in the application repository; functional .github issue/PR templates stay in the Git repository but are excluded from distributed source archives, and embedded legal notices use THIRD-PARTY-NOTICES.txt. +- Added a current Simplified Chinese usage guide and language navigation in the Wiki, based on the classic Shadowsocks Windows Chinese guide structure but updated for Reborn 5.2.31. +- Fixed duplicate Start-with-Windows launches by combining stale HKCU Run-entry cleanup with an executable-identity-independent process gate; legacy-only Run entries are migrated, disabling autostart removes stale entries too, and hidden startup activations no longer show an already-running dialog. +- Disabled generated FodyWeavers.xsd IntelliSense schemas, removed stale generated copies and removed schemaLocation references from FodyWeavers.xml to stop recurring XSD/XML IDE/source-tree churn. +- Fixed a false-positive release-validator regex around the updater download promotion; the real Windows stream-disposal contract remains covered by DownloadedAssetIsClosedBeforePromotion and has been manually verified with a previous-version to newer-version update test. +- Fixed automatic update downloads on Windows: the .download stream is now disposed before File.Move promotion; SHA-256 sidecars accept both canonical sha256sum format and a plain 64-hex digest +- Fixed DNSCrypt loopback-port allocation on Windows hosts with transport-specific dynamic/excluded port ranges by selecting UDP-first, verifying the same port over TCP, and using a dual-bind fallback below the default dynamic range. +- Relaxed the Bouncy Castle Bzip2 notice validator to verify the legal contract (`modified Bzip2` + `Apache License 2.0`) instead of one obsolete wording. +- Fixed the Phase 10 NetworkService version release-validator check after version response ownership moved from `Program.cs` to `NetworkServiceResponses.cs`; validation now also requires the versioned-ping regression test. +- Fixed invalid multiline PowerShell boolean expressions in `Validate-DnsCrypt.ps1` that were caught by the release parser gate. +- Fixed a release-validator PowerShell interpolation parser error and added a CI/release PowerShell parser gate for every `packaging/*.ps1` script. +- Synchronized README/Wiki architecture, security, storage, WinUI and release-checklist documentation with the final 5.2.31 contracts +- Fixed the release validator to match the current selectable RichTextBlock Logs viewer instead of retired ListView implementation tokens +- Fixed release-blocking SelfUpdater compile errors in UpdateChecker/SelfUpdater +- Fixed DNSCrypt TOML regression test false-positive and completed resolver-filter localization across all shipped locales +- Synchronized product, helper and WinUI version metadata for the 5.2.31 release +- Hardened release validation for updater, ss:// registration, Windows 10 19041 compatibility and one-file packaging +- Pinned release builds to .NET SDK 10.0.303+ / runtime 10.0.11 security baseline and made NuGet audit unavailability and vulnerability findings release-blocking +- Hardened updater handoff against staged-payload tampering across the UAC launch window and verify the published EXE FileVersion before packaging +- Refreshed GitHub release defaults, release checklist and legal metadata +- Full release notes: https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog + +Development snapshot - network/update hardening - 2026-08-21 +- Fixed SOCKS5 forward-proxy authentication and fragmented/domain reply handling (RFC 1928/1929) +- Hardened HTTP CONNECT forwarding against partial writes, fragmented headers and coalesced tunnel payload; product User-Agent replaces stale browser impersonation +- Hardened UDP relay destination parsing/strategy selection, association recovery and resource disposal; removed the borrowed legacy LRU implementation +- Fixed logging settings so verbose/plugin/DNS switches immediately affect runtime output and the live viewer +- Fixed Automatic DNSCrypt resolver filtering and removed the legacy Cloudflare-only runtime fallback +- Prepared dnscrypt-proxy updates now verify the candidate version first, resolve an eligible server from its signed catalog, and health-check that concrete runtime before activation +- Resolver country metadata is now derived only from endpoint IP GeoIP; text/city/Anycast heuristics are not used +- Completed release documentation and localization validation +- Full release notes: https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog + +Development snapshot - DNSCrypt/Admin DNS - 2026-08-18 +- Added managed DNSCrypt Proxy with signed downloads, automatic updates and resolver selection +- Added Admin Mode UDP/TCP DNS interception with fail-closed and Game Mode integration +- Added final DNSCrypt lifecycle, Clean Mode, bootstrap-recursion and release hardening +- Pinned WinDivert x64 payload hashes and added fragment-safe routing plus FLOW-layer PID attribution +- Full release notes: https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog + +Development snapshot - SIP003/plugins - 2026-08-18 +- Added managed SIP003 plugin catalog/install/update flow and manual ZIP/TAR.GZ import +- Servers UI now shows Server Name above IP and selects installed plugins from a dropdown +- Finalized LocalAppData/Clean Mode storage, localization, WinUI polish and release validation +- Full release notes: https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog + +Reborn modernization baseline - 2026-08-15 +- Reborn fork: .NET 10 / Windows 10 19041+ / x64-only +- Managed HTTP proxy replaces Privoxy; WinINet replaces sysproxy helpers +- Admin Mode adds transparent TCP/UDP routing through WinDivert +- Game Mode is automatic and temporarily disables WinDivert for configured applications +- Full release notes: https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog + 4.4.1.0 2022-02-08 - Add plain/none ciphers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f11709049..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,18 +0,0 @@ -How to Contribute -================= - -Pull Requests -------------- - -1. Pull requests are welcome. -2. Make sure to pass the unit tests. Write unit tests for new modules if -needed. -3. Search before sending new pull request. - -Issues ------- - -1. **DO NOT post question about connection problem in issue tracker**, read [Troubleshooting]. -2. Search before sending new issue. - -[Troubleshooting]: https://github.com/shadowsocks/shadowsocks-windows/wiki/Troubleshooting diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..6f8c879fa --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,20 @@ + + + 5.2.31 + false + + 10.0-recommended + true + + + false + all + + $(WarningsAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + + diff --git a/LICENSE.txt b/LICENSE.txt index 4be89ce95..e74758f5a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,26 @@ -shadowsocks-csharp -================== +Shadowsocks Reborn +================= + +Copyright (C) 2015 clowwindy +Copyright (C) 2020 Shadowsocks Project +Copyright (C) 2026 Shadowsocks Reborn contributors + +Shadowsocks Reborn is a modified continuation of shadowsocks-windows. +The program is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +The program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +SPDX-License-Identifier: GPL-3.0-or-later +Project source: https://github.com/SlimRG/shadowsocks-reborn +Upstream source: https://github.com/shadowsocks/shadowsocks-windows + +The GNU General Public License version 3 terms preserved from the upstream project follow. GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 @@ -622,437 +643,3 @@ Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS - -Copyright (C) 2015 clowwindy -Copyright (C) 2020 Shadowsocks Project - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - - -3rd party projects -================== - -Privoxy ------------------- - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - - -mbed TLS --------- - -https://tls.mbed.org - -License: https://raw.githubusercontent.com/ARMmbed/mbedtls/master/LICENSE - - -Newtonsoft.Json ----------- - -https://raw.githubusercontent.com/JamesNK/Newtonsoft.Json/master/LICENSE.md - -The MIT License (MIT) - -Copyright (c) 2007 James Newton-King - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -ZXing ------ - -Copyright 2007 ZXing authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -libsodium ---------- - -Copyright (c) 2013-2015 -Frank Denis - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..df55119df --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/OPENSSL-GUIDE b/OPENSSL-GUIDE deleted file mode 100644 index 42eaa8fee..000000000 --- a/OPENSSL-GUIDE +++ /dev/null @@ -1,15 +0,0 @@ -OpenSSL library guide for VS2017 - -# Read NOTES.WIN and NOTES.PERL - -# use Visual Studio native tools command prompt -# use activeperl, install NASM assembler -ppm install dmake - -# Win32 x86 -set PATH=D:\NASM-32;%PATH% -perl Configure VC-WIN32 --release --prefix=C:\Users\home\Downloads\openssl-1.1.0g\x86-build --openssldir=C:\Users\home\Downloads\openssl-1.1.0g\x86-install -nmake -nmake test -# to rebuild -nmake distclean diff --git a/README.md b/README.md index c4b328f6a..dbee492d2 100644 --- a/README.md +++ b/README.md @@ -1,170 +1,200 @@ -[logo] Shadowsocks for Windows -======================= - -[![Build Status]][Appveyor] - -[中文说明] - -## Features - -1. System proxy configuration -2. PAC mode and global mode -3. [GeoSite] and user rules -4. Supports HTTP proxy -5. Supports server auto switching -6. Supports UDP relay (see Usage) -7. Supports plugins - -## Downloads - -Download the latest release from [release page]. - -## Requirements - -.NET Framework 4.8 or higher, Microsoft [Visual C++ 2015 Redistributable] (x86) . - -## Basics - -1. Find Shadowsocks icon in the notification tray -2. You can add multiple servers in servers menu -3. Select `Enable System Proxy` menu to enable system proxy. Please disable other -proxy addons in your browser, or set them to use system proxy -4. You can also configure your browser proxy manually if you don't want to enable -system proxy. Set Socks5 or HTTP proxy to 127.0.0.1:1080. You can change this -port in `Servers -> Edit Servers` - -## PAC - -- The PAC rules are generated from the geosite database in [v2fly/domain-list-community](https://github.com/v2fly/domain-list-community). -- Generation modes: whitelist mode and blacklist mode. -- Domain groups: `geositeDirectGroups` and `geositeProxiedGroups`. - - `geositeDirectGroups` is initialized with `cn` and `geolocation-!cn@cn`. - - `geositeProxiedGroups` is initialized with `geolocation-!cn`. -- To switch between different modes, modify the `geositePreferDirect` property in `gui-config.json` - - When `geositePreferDirect` is false (default), PAC works in whitelist mode. Exception rules are generated from `geositeDirectGroups`. Unmatched domains goes through the proxy. - - When `geositePreferDirect` is true, PAC works in blacklist mode. Blocking rules are generated from `geositeProxiedGroups`. Exception rules are generated from `geositeDirectGroups`. Unmatched domains are connected to directly. -- Starting from 4.3.0.0, shadowsocks-windows defaults to whitelist mode with Chinese domains excluded from connecting via the proxy. -- The new default values make sure that: - - When in whitelist mode, Chinese domains, including non-Chinese companies' Chinese CDNs, are connected to directly. - - When in blacklist mode, only non-Chinese domains goes through the proxy. Chinese domains, as well as non-Chinese companies' Chinese CDNs, are connected to directly. - -### User-defined rules - -- To define your own PAC rules, it's recommended to use the `user-rule.txt` file. -- You can also modify `pac.txt` directly. But your modifications won't persist after updating geosite from the upstream. - -For Windows10 Store and related applications, please execute the following command under Admin privilege: -``` -netsh winhttp import proxy source=ie -``` - -## Server Auto Switching - -1. Load balance: choosing server randomly -2. High availability: choosing the best server (low latency and packet loss) -3. Choose By Total Package Loss: ping and choose. Please also enable - `Availability Statistics` in the menu if you want to use this -4. Write your own strategy by implement IStrategy interface and send us a pull request! - -## UDP - -For UDP, you need to use SocksCap or ProxyCap to force programs you want -to be proxied to tunnel over Shadowsocks - -## Multiple Instances - -If you want to manage multiple servers using other tools like SwitchyOmega, -you can start multiple Shadowsocks instances. To avoid configuration conflicts, -copy Shadowsocks to a new directory and choose a different local port. - -## Plugins - -If you would like to connect to server via a plugin, please set the plugin's -path (relative or absolute) on Edit Servers form. -_Note_: Forward Proxy will not be used while a plugin is enabled. - -Details: -[Working with non SIP003 standard Plugin]. - -## Global hotkeys - -Hotkeys could be registered automatically on startup. -If you are using multiple instances of Shadowsocks, -you must set different key combination for each instance. - -### How to input? - -1. Put focus in the corresponding textbox. -2. Press the key combination that you want to use. -3. Release all keys when you think it is ready. -4. Your input appears in the textbox. - -### How to change? - -1. Put focus in the corresponding textbox. -2. Press BackSpace key to clear content. -3. Re-input new key combination. - -### How to deactivate? - -1. Clear content in the textbox that you want to deactivate, -if you want to deactivate all, please clear all textboxes. -2. Press OK button to confirm. - -### Meaning of label color - -- Green: This key combination is not occupied by other programs and register successfully. -- Yellow: This key combination is occupied by other programs and you have to change to another one. -- Transparent without color: The initial status. - -## Server Configuration - -Please visit [Servers] for more information. - -## Experimental - -[Experimental Features] - -## Development - -1. Visual Studio 2019 & .NET Framework 4.8 SDK are required. -2. It is recommended to share your idea on the Issue Board before you start to work, -especially for feature development. - -## License - -[GPLv3] - -## Open Source Components / Libraries - -``` -Caseless.Fody (MIT) https://github.com/Fody/Caseless -Costura.Fody (MIT) https://github.com/Fody/Costura -Fody (MIT) https://github.com/Fody/Fody -GlobalHotKey (GPLv3) https://github.com/kirmir/GlobalHotKey -MdXaml (MIT) https://github.com/whistyun/MdXaml -Newtonsoft.Json (MIT) https://www.newtonsoft.com/json -ReactiveUI.WPF (MIT) https://github.com/reactiveui/ReactiveUI -ReactiveUI.Events.WPF (MIT) https://github.com/reactiveui/ReactiveUI -ReactiveUI.Fody (MIT) https://github.com/reactiveui/ReactiveUI -ReactiveUI.Validation (MIT) https://github.com/reactiveui/ReactiveUI.Validation -WPFLocalizationExtension (MS-PL) https://github.com/XAMLMarkupExtensions/WPFLocalizationExtension/ -ZXing.Net (Apache 2.0) https://github.com/micjahn/ZXing.Net - -libsscrypto (GPLv2) https://github.com/shadowsocks/libsscrypto -Privoxy (GPLv2) https://www.privoxy.org -Sysproxy () https://github.com/Noisyfox/sysproxy -``` - - - -[Appveyor]: https://ci.appveyor.com/project/celeron533/shadowsocks-windows -[Build Status]: https://ci.appveyor.com/api/projects/status/tfw57q6eecippsl5/branch/master?svg=true -[release page]: https://github.com/shadowsocks/shadowsocks-csharp/releases -[GeoSite]: https://github.com/v2fly/domain-list-community -[Servers]: https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#linux--server-side -[中文说明]: https://github.com/shadowsocks/shadowsocks-windows/wiki/Shadowsocks-Windows-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E -[Visual C++ 2015 Redistributable]: https://www.microsoft.com/en-us/download/details.aspx?id=53840 -[GPLv3]: https://github.com/shadowsocks/shadowsocks-windows/blob/master/LICENSE.txt -[Working with non SIP003 standard Plugin]: https://github.com/shadowsocks/shadowsocks-windows/wiki/Working-with-non-SIP003-standard-Plugin -[Experimental Features]: https://github.com/shadowsocks/shadowsocks-windows/wiki/Experimental \ No newline at end of file +# shadowsocks-reborn for Windows + +Shadowsocks logo + +**English** | [Русский](README.ru.md) | [简体中文](README.zh-CN.md) | [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) + +`shadowsocks-reborn` is a Windows-focused continuation of the classic Shadowsocks for Windows v4 client. Current builds target **.NET 10**, **WinUI 3 / Windows App SDK**, **x64**, and Windows 10 build 19041 or newer. + +> This is an independent fork, not the upstream `shadowsocks/shadowsocks-windows` repository. + +If no complete Shadowsocks server is configured, server-dependent DNS, PAC and Windows system-proxy enable actions stay unavailable; adding a valid server unlocks them. A new DNSCrypt configuration starts with every first-use option enabled except IPv6, while any DNSCrypt settings already persisted on disk are preserved exactly during configuration-schema upgrades. Long-running shell operations such as Start on Boot, Administrator capture/UAC and update actions display explicit busy progress. WinUI navigation and privilege glyphs use the Windows 10-compatible Segoe MDL2 Assets set. + +## Current release: 5.2.31 + +Release 5.2.31 includes working Administrator-mode transparent DNS interception/routing for the `System`, `Direct`, `Proxy`, `CustomDoh` and `DnsCrypt` policies, DNSCrypt/DoH management with ODoH disabled, hardened automatic self-update, duplicate-autostart protection, and the current WinUI 3 shell. It also tightens CI/release gates around .NET servicing, NuGet vulnerability auditing, Windows 10 build 19041 compatibility and published EXE version metadata. + +See [Changelog](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog) for the full release history. + +## Highlights + +- Native WinUI 3 desktop interface; WinForms and WPF are no longer part of the product UI. +- Unpackaged, self-contained, win-x64, single-file distribution. +- Release layout contains exactly `Shadowsocks.exe`. +- File-backed per-user configuration under `%LOCALAPPDATA%\Shadowsocks`; the product directory is not used as mutable storage. +- Settings, caches, PAC data, logs and startup copy under `%LOCALAPPDATA%\Shadowsocks` in normal mode. +- Rufus-style Clean Mode (`...p.exe`) redirects all writable application state to a disposable `%TEMP%\Shadowsocks\Clean\...` session. +- Managed HTTP/1.1 proxy and HTTPS `CONNECT`; Privoxy/sysproxy are retired. +- Per-application `Proxy`, `Direct` and `Block` routing. +- Transparent TCP/UDP capture in Admin Mode through WinDivert. +- Automatic Game Mode that suspends Admin capture while configured applications are running. +- Game discovery suggestions for Steam, Epic Games, GOG and Xbox libraries; manual rules remain available. +- SIP003 plugin manager with built-in `xray-plugin`, `v2ray-plugin` and `qtun` choices plus manual ZIP/TAR.GZ import; `ss://` import offers to install a missing known catalog plugin before the server is saved; trusted catalog packages auto-check for updates once per day when not in use and can also be checked manually. +- UDP relay, QR import/export, hotkeys and a single embedded CSV localization catalog. + +## Traffic modes + +Only two modes are selectable: + +- **User Mode** — no elevation and no NetworkService extraction. Routing applies to traffic that reaches the local/system proxy. +- **Admin Mode** — requests UAC, materializes the embedded `Shadowsocks.NetworkService.exe` under the active storage root, validates it, launches it elevated, and enables transparent TCP/UDP capture through WinDivert. + +**Game Mode is an automatic compatibility state, not a third traffic mode.** When Admin Mode is selected and a configured game/application starts, WinDivert capture is suspended. Admin capture is restored automatically after the application exits. + +The Traffic page exposes configured/runtime mode, NetworkService state, WinDivert state, TCP/UDP capture state and redirect ports. + +## Requirements + +- Windows 10 2004 / build 19041 or newer, or Windows 11; +- x64 Windows; +- .NET 10 SDK 10.0.303 or newer when building from source (release builds require the .NET 10.0.11 security baseline). + +The published product is self-contained and does not require a separately installed .NET runtime. + +## Solution layout + +- `Shadowsocks.Core` — protocol, encryption, configuration model, PAC/GeoSite logic, routing models, localization and storage abstractions. +- `Shadowsocks.Windows` — file-storage bootstrap, WinINet/system proxy, startup, UAC/Admin capture, WinDivert runtime, hotkeys and other Windows integration. +- `Shadowsocks.Windows.WinUI` — standalone WinUI-specific shell/tray/QR/power integration with no Core/Windows project dependency. +- `Shadowsocks.WinUI` — WinUI 3 application shell and product publish project (`Shadowsocks.exe`). +- `Shadowsocks.NetworkService` — isolated elevated WinDivert helper embedded into release builds. +- `Shadowsocks.UnitTests` — Core/Windows tests without presentation dependencies. + +See [Architecture](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Architecture) for project boundaries and runtime flow. + +## Build + +`Directory.Build.props` is the single source of the three-part product version. `ApplicationInfo.Version`, manifests and release notes are validated against it. + +Use .NET SDK 10.0.303 or newer on Windows: + +```powershell +dotnet restore .\shadowsocks-reborn.sln -p:Platform=x64 -r win-x64 +dotnet build .\shadowsocks-reborn.sln -c Release -p:Platform=x64 -m:1 --no-restore +dotnet test .\Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj -c Release -p:Platform=x64 --no-build +``` + +Publish the product: + +```powershell +dotnet restore .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 +dotnet publish .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -c Release -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 --self-contained true --no-restore +``` + +Or build the release ZIP and SHA-256 file: + +```powershell +.\packaging\Validate-Repository.ps1 +.\packaging\Build-Release.ps1 +``` + +The canonical GitHub Release asset is `Shadowsocks-win-x64.zip`, with `Shadowsocks-win-x64.zip.sha256` published beside it. + +The final publish directory and release ZIP must contain exactly: + +```text +Shadowsocks.exe +``` + +DLLs, PDBs, runtime JSON files, icons and `Shadowsocks.NetworkService.exe` sidecars are rejected by release validation. + +## Storage and startup + +Normal mode stores all persistent application-owned state under: + +```text +%LOCALAPPDATA%\Shadowsocks +``` + +The configuration backend is `%LOCALAPPDATA%\Shadowsocks\settings.json` with an atomic `settings.backup.json`. SIP003 packages installed from the Plugins page are stored in `%LOCALAPPDATA%\Shadowsocks\Plugins` and resolved by plugin id when a server starts. Catalog packages retain trusted GitHub release provenance and may be updated automatically once per day while not in use; when both release tags are parseable versions, a lower candidate version is rejected rather than installed. Manual ZIP/TAR.GZ imports never opt into background network updates. Old `HKCU\Software\Shadowsocks Reborn\Settings` values are ignored. Localization uses only the `i18n.csv` embedded inside `Shadowsocks.exe`; no second catalog is extracted. + +The Settings page shows the active storage path and provides a single **Open** button that opens that directory in either normal or Clean Mode. + +Rename the executable so its file name ends in `p` before `.exe` to start **Clean Mode**, for example `Shadowsocksp.exe` or `Shadowsocks-cleanp.exe`. Clean Mode redirects settings, caches, PAC data, logs, installed plugin packages, runtime files and helper/component-update working data to a unique `%TEMP%\Shadowsocks\Clean\...` session and removes that session best-effort on Quit. Start with Windows is unavailable in Clean Mode. + +In normal mode, Start with Windows copies the verified product EXE to `%LOCALAPPDATA%\Shadowsocks\Startup\Shadowsocks.exe`; the Windows Run integration points to that stable copy. Startup migrates a legacy-only matching `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry to the canonical entry, removes stale duplicates, and disabling Start with Windows removes matching legacy entries as well. An executable-identity-independent process gate prevents the LocalAppData startup copy and the original EXE from both starting a controller and racing for the same local port. Windows Restart Manager is kept mutually exclusive with Start with Windows. Both mechanisms continuously record the shell's actual UI state using `--start-visible` or `--start-hidden`, so reboot/logon restores the same state that existed before shutdown: an open window returns open, while a tray-only session stays in the tray. `WM_QUERYENDSESSION` snapshots the state before Windows closes the HWND, preventing system shutdown from being misinterpreted as a user close-to-tray action. + +See [Storage policy](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Storage-Policy) for the complete layout and cleanup rules. + +## Application updates + +Update downloads are staged as `*.download`; the file handle is closed before the temporary file is atomically promoted to its canonical name on Windows. SHA-256 sidecars may contain either ` Shadowsocks-win-x64.zip` or just the 64-character hexadecimal digest. + +Application updates are automatic by default. After startup, the app checks GitHub Releases and selects only a **strictly newer numeric version** than the installed build (`5.2.22 < 5.2.31`, so 5.2.22 is ignored; `5.2.32 > 5.2.31`, so 5.2.32 is eligible), requires the exact `Shadowsocks-win-x64.zip` plus `.sha256` sidecar, verifies the SHA-256, ZIP layout and payload file version, then stages the new single-file EXE under `%TEMP%\Shadowsocks\Updates`. Before launch, the staged updater receives its own SHA-256; the source process keeps that staged file open without write/delete sharing across `Process.Start`/UAC and passes the digest through the internal update handoff. The updater re-verifies its own staged image before replacing the installed product. It then waits for the current process to exit, preserves rollback state, replaces the product EXE and starts the installed new copy. That installed copy removes the temporary updater transaction. If Start with Windows launched the LocalAppData startup copy, update eligibility uses the higher of the running copy version and the recorded primary EXE FileVersion, so a stale startup copy cannot treat an older release as newer. The updater then checks the target FileVersion again immediately before replacement and refuses equal-version or downgrade payloads. The recorded primary EXE is updated instead of only the startup copy. + +## Embedded NetworkService + +Release builds publish `Shadowsocks.NetworkService` as a self-contained single-file helper and embed it into `Shadowsocks.exe`. + +User Mode never extracts the helper. Admin Mode extracts it on demand below the active storage root (`%LOCALAPPDATA%\Shadowsocks\Temp\NetworkService\...` in normal mode, the Clean Mode session in Clean Mode). Extraction is serialized, SHA-256 validated and followed by an elevated control-pipe version handshake. The helper is guarded while in use and removed best-effort when the broker stops; stale runtime directories are pruned later. + +Development builds may use a separate helper from the build output. That fallback is not part of the release package. + +## WinDivert verification + +Admin Mode is reported active only after the elevated capture process successfully opens WinDivert. A successful startup logs a message similar to: + +```text +WinDivert capture confirmed (start): Admin capture active.; TCP redirect port=..., UDP redirect port=... +``` + +For a functional test, add `curl.exe -> Block`, disable the Windows system proxy temporarily, and compare: + +```cmd +curl.exe -4 --noproxy "*" https://example.com +``` + +The direct request should bypass application routing in User Mode and be blocked in Admin Mode. + +## PAC, HTTP forwarding and DNS + +Local GeoSite + EasyList/ABP routing is **always managed C#**. The Local PAC file contains only a minimal `FindProxyForURL` funnel into `ManagedHttpProxyService`; it does not evaluate filter rules. Application rules run first, then the immutable GeoSite + `user-rule.txt` snapshot in `Shadowsocks.Routing.FilterEngine` makes the authoritative `DIRECT`/`PROXY` decision. The historical bundled `abp.js`, the compiled-PAC compatibility backend, the backend selector, and the executable custom `abp.txt` override have all been removed. When the settings schema changes, removed properties are pruned automatically during canonical migration. **Online PAC is separate:** if explicitly enabled, the user-selected external PAC program remains authoritative because arbitrary PAC JavaScript cannot be losslessly converted to ABP/EasyList rules. The PAC / GeoSite page exposes **Open user-rule.txt** in Local mode; user rules are evaluated before GeoSite defaults and applied automatically when the file changes. Local managed-routing diagnostics, user-rule editing and GeoSite source controls are hidden while Online PAC is active. + +Administrator Mode now sends the same managed rule set to the elevated NetworkService. Ordinary unmatched TCP is marked `Deferred` at SYN time and reflected into the transparent relay; the relay inspects only the initial clear-text routing metadata to obtain HTTP `Host` or TLS ClientHello SNI, then opens either a direct connection or the Shadowsocks SOCKS5 path. There is **no TLS decryption, certificate installation or MITM**. HTTP requests can be matched with path/query information; TLS/SNI routing is hostname-only. Explicit application rules retain priority. UDP, TCP DNS/53 and protocols without usable Host/SNI keep deterministic fallback behavior; rule-file changes coalesce and restart only the active capture child through the already-elevated broker. + +`ManagedHttpProxyService` handles HTTP/1.1 and HTTPS `CONNECT`. HTTPS is tunneled as bytes, so HTTP/2 negotiated inside TLS does not require a local HTTP/2 parser. FTP gatewaying is not implemented. + +### DNS policy contract + +The current 5.2.31 configuration/IPC contract contains five DNS policy values, and Administrator Mode implements transparent routing for classic DNS captured on UDP/TCP port 53 through `Shadowsocks.NetworkService` + WinDivert: + +| Policy | Current behavior | +| --- | --- | +| `System` | Keeps captured classic DNS on its original/system destination without policy-specific redirection. | +| `Direct` | Can keep the original destination or redirect captured DNS to configured primary/fallback IPv4/IPv6 resolvers. The selected DNS endpoint can optionally be reached through the local Shadowsocks SOCKS5 path. | +| `Proxy` | Routes captured classic DNS through Shadowsocks. | +| `CustomDoh` | Bridges captured DNS wire messages to the configured HTTPS DoH endpoint; the DoH upstream can independently be routed through Shadowsocks. | +| `DnsCrypt` | Redirects captured DNS to the dynamically allocated local `dnscrypt-proxy` listener and operates fail-closed when the secure runtime is unavailable. | + +Transparent system-wide interception is therefore **implemented in 5.2.31**, but it requires **Administrator Mode**. User Mode still uses Shadowsocks-managed DNS/DNSCrypt where applicable, but it does not intercept arbitrary system DNS. Automatic Game Mode pauses system-wide interception while Admin capture is suspended. The transparent interception covers classic DNS on UDP/TCP 53; application-internal DoH, DoT and DoQ are not generically intercepted. + +The DNS page manages the optional signed `dnscrypt-proxy` component. `Automatic` selects a concrete DNSCrypt/DoH resolver from the signed public catalog using DNSSEC, no-log, unfiltered and address-family constraints, then pins it in `server_names`. Resolver-catalog maintenance resolves source hostnames through **Cloudflare DoH over Shadowsocks**, with **Google DoH over Shadowsocks** as fallback, downloads the catalog and `.minisig` through the same tunnel, verifies the pinned DNSCrypt Minisign key, and only then exposes the local authenticated cache to `dnscrypt-proxy`. The catalog profile and active runtime both use `bootstrap_resolvers = []` and `ignore_system_dns = true`; the catalog profile additionally uses `urls = []`, so dnscrypt-proxy cannot fall back to remote source lookup or system/plaintext bootstrap. `Manual` exposes signed DNSCrypt and DoH entries with protocol/country/address-family/privacy filters; ODoH remains disabled. DNSCrypt component release discovery, ZIP download and Minisign download also use this Cloudflare→Google DoH-over-Shadowsocks transport, including GitHub redirect hosts, so installation/update does not intentionally depend on the Windows resolver for `api.github.com` or release assets. + +On first enable, DNSCrypt download/verification/install/activation keeps a dedicated animated operation card visible; unavailable DNSCrypt settings stay hidden until activation completes. + +The DNS page also provides `Test DNSCrypt` and a DNS privacy self-test. The local listener allocator verifies that one loopback port is usable by both UDP and TCP. DNSCrypt PID/port changes are propagated to NetworkService at runtime. DNSCrypt is fail-closed during startup, restart, recovery and runtime failure instead of silently downgrading intercepted traffic to plaintext system DNS. Fragmented datagrams that would require DNS/proxy rewriting are dropped as a whole so later fragments cannot bypass policy; direct fragmented traffic remains direct. + +WinDivert 2.2.2 is downloaded only from the pinned official release URL; the extracted x64 DLL/driver must match release-pinned SHA-256 digests before loading. A read-only FLOW observer supplies endpoint PID ownership for NETWORK-layer routing, with IP Helper lookup as fallback. + +DNSCrypt Automatic mode prefers a compatible resolver in the country of the active Shadowsocks server and falls back to the best compatible signed-catalog resolver. Resolver country is derived only from endpoint IP GeoIP data; resolver names/descriptions are not treated as geographic metadata. Manual resolver latency is measured asynchronously and the active resolver uses dnscrypt-proxy's RTT when available. Automatic component update checks are persisted and run no more than once per 24 hours; initial installation remains explicit. When DNSCrypt itself is routed through Shadowsocks, Shadowsocks/forward-proxy endpoints must be IP literals to prevent DNS bootstrap recursion. + +## Documentation + +- [User guide (English)](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-User-Guide) +- [Руководство пользователя (RU)](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-User-Guide) +- [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) + +Long-form Markdown is maintained in this dedicated GitHub Wiki rather than duplicated under the application repository. GitHub issue/PR templates remain under `.github/` because GitHub requires those paths. + + +- [Architecture](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Architecture) — project boundaries and runtime architecture. +- [Storage policy](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Storage-Policy) — LocalAppData, Clean Mode, component staging and application self-update storage rules. +- [Windows 11 / WinUI guide](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Windows-11-WinUI-Guide) — current WinUI design/implementation rules. +- [Contributing](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Contributing) — development rules. +- [Release checklist](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Release-Checklist) — release validation. +- [Security](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Security) — security reporting and sensitive areas. +- [Changelog](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-Changelog) — fork changes; upstream history remains in [`CHANGES`](https://github.com/SlimRG/shadowsocks-reborn/blob/main/CHANGES). + +## License + +`shadowsocks-reborn` is distributed under **GPL-3.0-or-later**. See [License](LICENSE.txt) and the authoritative [LICENSE.txt](https://github.com/SlimRG/shadowsocks-reborn/blob/main/LICENSE.txt). Third-party components retain their own licenses; see [Third-party notices](THIRD-PARTY-NOTICES.txt). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 000000000..58e9e741e --- /dev/null +++ b/README.ru.md @@ -0,0 +1,200 @@ +# shadowsocks-reborn для Windows + +Shadowsocks logo + +[English](README.md) | **Русский** | [简体中文](README.zh-CN.md) | [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) + +`shadowsocks-reborn` — Windows-ориентированное продолжение классического Shadowsocks for Windows v4. Актуальные сборки используют **.NET 10**, **WinUI 3 / Windows App SDK**, только **x64** и поддерживают Windows 10 build 19041 или новее. + +> Это независимый fork, не upstream `shadowsocks/shadowsocks-windows`. + +Если нет ни одного полностью настроенного Shadowsocks-сервера, зависимые от сервера DNS, PAC и включение системного proxy Windows недоступны; после добавления валидного сервера они разблокируются. Для новой конфигурации DNSCrypt при первом использовании включены все параметры, кроме IPv6, но уже сохранённые DNSCrypt-настройки при миграциях схемы не изменяются. Длительные операции оболочки — Start on Boot, переключение Administrator capture/UAC и обновления — показывают явный индикатор выполнения. Навигационные и privilege-значки WinUI используют совместимый с Windows 10 набор Segoe MDL2 Assets. + +## Текущий релиз: 5.2.31 + +В 5.2.31 уже реализован прозрачный перехват и DNS-routing в Administrator Mode для политик `System`, `Direct`, `Proxy`, `CustomDoh` и `DnsCrypt`. Релиз также включает управление DNSCrypt/DoH при отключённом ODoH, защищённое автоматическое обновление, защиту от двойного запуска при автозагрузке и актуальную WinUI 3 оболочку. CI/release gates дополнительно контролируют .NET servicing baseline, NuGet vulnerability audit, совместимость с Windows 10 build 19041 и версию опубликованного EXE. + +Полная история изменений — в [История изменений](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Changelog). + +## Основное + +- WinUI 3 — единственный desktop UI; WinForms и WPF больше не входят в продукт. +- Unpackaged, self-contained, win-x64, single-file deployment. +- В release находится только `Shadowsocks.exe`. +- Постоянные пользовательские настройки хранятся в `%LOCALAPPDATA%\Shadowsocks\settings.json`; каталог EXE не используется как изменяемое хранилище. +- В обычном режиме настройки, PAC/GeoSite cache, PAC-данные, logs и startup copy находятся в `%LOCALAPPDATA%\Shadowsocks`. +- Clean Mode по Rufus-схеме (`...p.exe`) переносит все изменяемые данные в одноразовый сеанс `%TEMP%\Shadowsocks\Clean\...`. +- Managed HTTP/1.1 proxy и HTTPS `CONNECT`; Privoxy/sysproxy удалены. +- Маршрутизация приложений: `Proxy`, `Direct`, `Block`. +- Transparent TCP/UDP capture в Admin Mode через WinDivert. +- Автоматический Game Mode временно останавливает Admin capture при запуске заданного приложения. +- Страница игр предлагает найденные Steam, Epic Games, GOG и Xbox игры; ручное добавление правил сохранено. +- Менеджер SIP003-плагинов со встроенным выбором `xray-plugin`, `v2ray-plugin`, `qtun` и ручным импортом ZIP/TAR.GZ; при импорте `ss://` с известным отсутствующим plugin приложение предлагает установить его до сохранения сервера; доверенные catalog-пакеты автоматически проверяются на обновления раз в сутки, когда не используются, и поддерживают ручную проверку. +- UDP relay, QR import/export, hotkeys и единственный embedded CSV-каталог локализации. + +## Режимы трафика + +Выбираются только два режима: + +- **User Mode** — без UAC и без извлечения NetworkService. Правила маршрутизации применяются к трафику, который приходит в локальный/system proxy. +- **Admin Mode** — запрашивает UAC, извлекает embedded `Shadowsocks.NetworkService.exe` под активный storage-root, проверяет его, запускает elevated и включает transparent TCP/UDP capture через WinDivert. + +**Game Mode — не третий режим трафика, а автоматическое runtime-состояние.** Если выбран Admin Mode и запускается приложение из списка Game Mode, WinDivert capture временно останавливается. После завершения приложения Admin capture восстанавливается автоматически. + +Страница Traffic показывает configured/runtime mode, NetworkService, WinDivert, TCP/UDP capture и redirect ports. + +## Требования + +- Windows 10 2004 / build 19041 или новее, либо Windows 11; +- x64 Windows; +- .NET 10 SDK 10.0.303 или новее нужен только для сборки из исходников (release build требует security baseline .NET 10.0.11). + +Опубликованный продукт self-contained и не требует отдельно установленного .NET runtime. + +## Структура solution + +- `Shadowsocks.Core` — protocol, encryption, configuration model, PAC/GeoSite, routing models, localization и storage abstractions. +- `Shadowsocks.Windows` — file-storage bootstrap, WinINet/system proxy, startup, UAC/Admin capture, WinDivert runtime, hotkeys и Windows integration. +- `Shadowsocks.Windows.WinUI` — самостоятельная WinUI-specific библиотека shell/tray/QR/power integration без ProjectReference на Core/Windows. +- `Shadowsocks.WinUI` — WinUI 3 shell и проект, публикующий `Shadowsocks.exe`. +- `Shadowsocks.NetworkService` — изолированный elevated WinDivert helper, embedded в release build. +- `Shadowsocks.UnitTests` — тесты Core/Windows без зависимости от UI. + +Подробнее: [Архитектура](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Architecture). + +## Сборка + +Единственный источник трёхчастной product version — `Directory.Build.props`. `ApplicationInfo.Version`, manifests и release notes проверяются относительно него. + +Нужен .NET SDK 10.0.303 или новее под Windows: + +```powershell +dotnet restore .\shadowsocks-reborn.sln -p:Platform=x64 -r win-x64 +dotnet build .\shadowsocks-reborn.sln -c Release -p:Platform=x64 -m:1 --no-restore +dotnet test .\Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj -c Release -p:Platform=x64 --no-build +``` + +Product publish: + +```powershell +dotnet restore .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 +dotnet publish .\Shadowsocks.WinUI\Shadowsocks.WinUI.csproj -c Release -p:Platform=x64 -p:PublishProfile=FolderProfile -r win-x64 --self-contained true --no-restore +``` + +Либо release ZIP + SHA-256: + +```powershell +.\packaging\Validate-Repository.ps1 +.\packaging\Build-Release.ps1 +``` + +Каноническое имя GitHub Release asset: `Shadowsocks-win-x64.zip` (рядом публикуется `Shadowsocks-win-x64.zip.sha256`). + +Финальный publish directory и release ZIP должны содержать ровно: + +```text +Shadowsocks.exe +``` + +DLL, PDB, runtime JSON, ICO и отдельный `Shadowsocks.NetworkService.exe` запрещены validator-ом. + +## Хранение данных и автозагрузка + +В обычном режиме всё постоянное состояние приложения хранится под: + +```text +%LOCALAPPDATA%\Shadowsocks +``` + +Основной backend конфигурации — `%LOCALAPPDATA%\Shadowsocks\settings.json`, резервный документ — `settings.backup.json`. SIP003-плагины со страницы «Плагины» устанавливаются в `%LOCALAPPDATA%\Shadowsocks\Plugins` и выбираются для сервера по идентификатору. Пакеты из встроенного каталога сохраняют проверенное происхождение GitHub release и могут автоматически обновляться раз в сутки, пока плагин не используется; если оба release tag распознаются как версии, более старая candidate-версия отклоняется. Ручные ZIP/TAR.GZ-пакеты никогда не включаются в фоновое сетевое обновление. Старые значения `HKCU\Software\Shadowsocks Reborn\Settings` игнорируются. Локализация использует только `i18n.csv`, встроенный внутрь `Shadowsocks.exe`; второй файл больше не распаковывается. + +В Settings показывается активный путь хранилища и одна кнопка **Открыть**, которая открывает этот каталог как в обычном режиме, так и в Clean Mode. + +Если имя EXE заканчивается на `p` перед `.exe`, например `Shadowsocksp.exe` или `Shadowsocks-cleanp.exe`, включается **Clean Mode**. В нём настройки, кэши, PAC, логи, установленные плагины, runtime, helper и component-update/working-файлы пишутся в уникальный `%TEMP%\Shadowsocks\Clean\...` сеанс и удаляются best-effort при Quit. Автозагрузка в Clean Mode недоступна. + +В обычном режиме Start with Windows копирует проверенный EXE в `%LOCALAPPDATA%\Shadowsocks\Startup\Shadowsocks.exe`; Windows Run integration указывает только на эту стабильную копию. Если осталась только legacy-запись `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, она мигрируется в каноническую запись; дубли удаляются, а при отключении Start with Windows удаляются и совпадающие legacy-записи. Независимый от app-identity process gate не позволяет startup-copy и исходному EXE одновременно запустить два controller и столкнуться за один локальный порт. Windows Restart Manager взаимоисключён со Start with Windows. Оба механизма постоянно фиксируют фактическое состояние UI через `--start-visible` или `--start-hidden`, поэтому после перезагрузки/входа восстанавливается состояние до выключения: открытое окно снова открывается, а приложение из трея остаётся в трее. `WM_QUERYENDSESSION` фиксирует состояние до системного закрытия HWND, поэтому завершение Windows не ошибочно трактуется как пользовательское «закрыть в трей». + +Полная схема — в [Политика хранения](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Storage-Policy). + +## Обновление приложения + +Загрузки обновлений сначала сохраняются как `*.download`; файловый дескриптор закрывается до атомарного переименования временного файла в Windows. SHA-256 sidecar может содержать либо ` Shadowsocks-win-x64.zip`, либо только 64-символьный шестнадцатеричный хэш. + +Обновление приложения по умолчанию автоматическое. После запуска клиент проверяет GitHub Releases и выбирает только **строго более новую числовую версию**, чем установленная (`5.2.22 < 5.2.31`, поэтому 5.2.22 игнорируется; `5.2.32 > 5.2.31`, поэтому 5.2.32 подходит для обновления), требует точные `Shadowsocks-win-x64.zip` и `.sha256`, проверяет SHA-256, структуру ZIP и версию EXE, затем размещает новый single-file EXE в `%TEMP%\Shadowsocks\Updates`. Перед запуском staged updater получает собственный SHA-256; исходный процесс удерживает файл открытым без разрешения записи/удаления на время `Process.Start`/UAC и передаёт digest через внутренний update-handoff. Уже запущенный updater повторно проверяет собственный staged image до замены установленного EXE. Затем он ждёт завершения текущего процесса, сохраняет rollback-копию, заменяет основной EXE и запускает установленную новую копию. Новая копия удаляет временный updater и transaction. Если приложение было запущено из копии автозагрузки в LocalAppData, при выборе обновления используется большая из двух версий: версия запущенной startup-copy и FileVersion записанного основного EXE. Поэтому устаревшая startup-copy не может принять старый релиз за новый. Непосредственно перед заменой updater повторно сверяет FileVersion целевого EXE и отказывается от payload той же или более старой версии. Обновляется записанный основной EXE, а не только startup-copy. + +## Embedded NetworkService + +В product publish `Shadowsocks.NetworkService` собирается как self-contained single-file helper и встраивается в `Shadowsocks.exe`. + +В User Mode helper не извлекается. При включении Admin Mode он materialize-ится под активный storage-root (`%LOCALAPPDATA%\Shadowsocks\Temp\NetworkService\...` в обычном режиме и внутри Clean Mode session в Clean Mode). Извлечение сериализовано, файл проверяется SHA-256, после UAC выполняется version handshake по control pipe. Пока broker работает, helper защищён от замены/удаления; после остановки каталог удаляется best-effort, а stale runtime очищается при следующих запусках. + +Development build может использовать отдельный helper из build output. В release package его нет. + +## Проверка WinDivert + +Admin Mode считается активным только после успешного `WinDivertOpen` в elevated capture process. Успешный запуск даёт лог примерно такого вида: + +```text +WinDivert capture confirmed (start): Admin capture active.; TCP redirect port=..., UDP redirect port=... +``` + +Для функционального A/B теста добавь `curl.exe -> Block`, временно отключи Windows system proxy и сравни: + +```cmd +curl.exe -4 --noproxy "*" https://example.com +``` + +В User Mode прямой запрос должен пройти мимо application routing, а в Admin Mode — блокироваться. + +## PAC, HTTP forwarding и DNS + +Маршрутизация Local GeoSite + EasyList/ABP теперь **всегда выполняется managed-кодом C#**. Файл Local PAC содержит только минимальный `FindProxyForURL`, направляющий трафик в `ManagedHttpProxyService`; правила фильтрации внутри PAC не исполняются. Сначала применяются application rules, затем неизменяемый snapshot GeoSite + `user-rule.txt` в `Shadowsocks.Routing.FilterEngine` принимает авторитетное решение `DIRECT`/`PROXY`. Исторический `abp.js`, compiled-PAC backend, переключатель backend и исполняемый пользовательский `abp.txt` полностью удалены. При изменении схемы настроек удалённые свойства автоматически вычищаются канонической миграцией. **Online PAC — отдельный режим:** если его явно включить, выбранный пользователем внешний PAC остаётся источником решения, поскольку произвольный PAC JavaScript нельзя без потерь преобразовать в правила ABP/EasyList. На странице **PAC / GeoSite** в Local mode доступна кнопка **Открыть user-rule.txt**: пользовательские правила имеют приоритет над GeoSite defaults и применяются автоматически при изменении файла. При включённом Online PAC карточки Local managed routing, user-rule и GeoSite скрываются. + +В Administrator Mode тот же managed rule set передаётся elevated NetworkService. Обычный TCP без явного application rule на SYN помечается `Deferred` и отражается в transparent relay; relay читает только начальные открытые routing-метаданные, извлекает HTTP `Host` или TLS ClientHello SNI и после этого открывает либо прямое соединение, либо SOCKS5 через Shadowsocks. **TLS не расшифровывается, сертификаты не устанавливаются, MITM отсутствует.** Для HTTP доступны path/query, для TLS/SNI — только hostname. Явные application rules сохраняют приоритет. UDP, TCP DNS/53 и протоколы без пригодного Host/SNI используют детерминированный fallback; быстрые изменения rule-файлов коалесцируются и перезапускают только capture child через уже elevated broker. + +`ManagedHttpProxyService` поддерживает HTTP/1.1 и HTTPS `CONNECT`. HTTPS идёт как byte tunnel, поэтому HTTP/2 внутри TLS не требует отдельного HTTP/2 parser в локальном proxy. FTP gateway не реализован. + +### Контракт DNS policy + +В актуальном 5.2.31 конфигурация/IPC содержит пять значений DNS policy, и в Administrator Mode прозрачная маршрутизация классического DNS по UDP/TCP порту 53 уже реализована через `Shadowsocks.NetworkService` + WinDivert: + +| Policy | Текущее поведение | +| --- | --- | +| `System` | Оставляет перехваченный классический DNS на исходном/системном destination без policy-specific redirect. | +| `Direct` | Может сохранить исходный destination либо перенаправить DNS на заданные основной/резервный IPv4/IPv6 resolver. Выбранный DNS endpoint при необходимости маршрутизируется через локальный SOCKS5 Shadowsocks. | +| `Proxy` | Отправляет перехваченный классический DNS через Shadowsocks. | +| `CustomDoh` | Преобразует перехваченные DNS wire messages в запросы к заданному HTTPS DoH endpoint; DoH upstream можно независимо отправлять через Shadowsocks. | +| `DnsCrypt` | Перенаправляет DNS в динамический локальный listener `dnscrypt-proxy` и работает fail-closed, если защищённый runtime недоступен. | + +То есть прозрачный системный DNS interception/routing в **5.2.31 реализован**, но для него требуется **Administrator Mode**. В User Mode остаётся DNS, которым управляет сам Shadowsocks/DNSCrypt, однако произвольный системный DNS не перехватывается. Автоматический Game Mode приостанавливает системный перехват вместе с Admin capture. Прозрачный перехват охватывает классический DNS по UDP/TCP 53; внутренний DoH/DoT/DoQ приложений универсально не перехватывается. + +Страница DNS управляет опциональным подписанным `dnscrypt-proxy`. `Automatic` выбирает конкретный DNSCrypt/DoH resolver из подписанного публичного каталога с учётом DNSSEC, no-log, отсутствия фильтрации и IPv4/IPv6, после чего фиксирует его в `server_names`. При обслуживании resolver-каталога имена источников разрешаются через **Cloudflare DoH по Shadowsocks**, с резервным **Google DoH по Shadowsocks**; сам каталог и `.minisig` скачиваются через тот же туннель и проверяются закреплённым Minisign-ключом DNSCrypt. Только после проверки локальная пара передаётся `dnscrypt-proxy`. И catalog-профиль, и активный runtime используют `bootstrap_resolvers = []` и `ignore_system_dns = true`, а catalog-профиль дополнительно содержит `urls = []`, поэтому dnscrypt-proxy не может откатиться к удалённому source lookup или системному/plaintext bootstrap. `Manual` показывает подписанные DNSCrypt/DoH resolver с фильтрами по протоколу, стране, address family и privacy-параметрам; ODoH остаётся отключён. Получение release metadata, ZIP и Minisign самого компонента DNSCrypt также использует этот транспорт Cloudflare→Google DoH поверх Shadowsocks, включая GitHub redirect-hosts, поэтому установка/обновление намеренно не зависит от Windows resolver для `api.github.com` и release assets. + +Есть `Test DNSCrypt` и отдельный DNS privacy self-test. Аллокатор локального listener проверяет, что один loopback-порт одновременно доступен по UDP и TCP. Изменения PID/порта DNSCrypt передаются NetworkService во время работы. При запуске, рестарте, восстановлении или сбое DNSCrypt действует fail-closed вместо скрытого отката перехваченного трафика на plaintext system DNS. Фрагментированные datagram, которым нужен DNS/proxy rewrite, отбрасываются целиком; direct-фрагменты остаются direct. + +WinDivert 2.2.2 скачивается только с закреплённого официального release URL; x64 DLL/driver должны совпасть с release-pinned SHA-256 до загрузки. Read-only FLOW observer передаёт NETWORK-router PID владельца endpoint, а IP Helper остаётся fallback. + +При первой активации DNSCrypt страница DNS оставляет видимой анимированную карточку установки/проверки, поэтому пользователь видит текущий этап вместо просто заблокированных настроек без индикации. + +Automatic DNSCrypt предпочитает совместимый resolver в стране активного Shadowsocks-сервера и при отсутствии совпадения выбирает лучший совместимый resolver подписанного каталога. Страна определяется только по GeoIP фактического endpoint IP; имя/description resolver не используются как географические признаки. В Manual задержка измеряется асинхронно, а для активного resolver при наличии используется RTT dnscrypt-proxy. Автоматическая проверка обновлений компонента выполняется не чаще раза в 24 часа; первичная установка остаётся явным действием пользователя. Если сам DNSCrypt маршрутизируется через Shadowsocks, адреса Shadowsocks/forward-proxy должны быть IP literals, чтобы исключить рекурсивный DNS bootstrap. + +## Документация + +- [Руководство пользователя (RU)](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-User-Guide) +- [User guide (English)](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-User-Guide) +- [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) + +Длинная Markdown-документация хранится только в отдельном GitHub Wiki и больше не дублируется в основном репозитории. `.github/*.md` остаются в основном репозитории, поскольку это функциональные issue/PR templates GitHub. + + +- [Архитектура](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Architecture) — архитектура проектов и runtime flow. +- [Политика хранения](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Storage-Policy) — правила LocalAppData, Clean Mode, staging компонентов и self-update приложения. +- [Руководство Windows 11 / WinUI](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Windows-11-WinUI-Guide) — актуальные правила WinUI. +- [Участие в разработке](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Contributing) — правила разработки. +- [Проверка перед релизом](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Release-Checklist) — проверки перед релизом. +- [Безопасность](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Security) — security reporting и чувствительные компоненты. +- [История изменений](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-Changelog) — изменения fork; история upstream остаётся в [`CHANGES`](https://github.com/SlimRG/shadowsocks-reborn/blob/main/CHANGES). + +## Лицензия + +`shadowsocks-reborn` распространяется по **GPL-3.0-or-later**. См. [страницу лицензии](LICENSE.txt) и юридически значимый [LICENSE.txt](https://github.com/SlimRG/shadowsocks-reborn/blob/main/LICENSE.txt). Сторонние компоненты сохраняют собственные лицензии; см. [уведомления сторонних компонентов](THIRD-PARTY-NOTICES.txt). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 000000000..6c35af9fc --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,189 @@ +# shadowsocks-reborn for Windows + +Shadowsocks logo + +[English](README.md) | [Русский](README.ru.md) | **简体中文** | [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) + +`shadowsocks-reborn` 是经典 Shadowsocks for Windows v4 客户端面向现代 Windows 的延续版本。当前构建基于 **.NET 10**、**WinUI 3 / Windows App SDK**,仅支持 **x64**,系统要求为 Windows 10 2004 / build 19041 或更高版本以及 Windows 11。 + +> 本项目是独立 fork,不是 `shadowsocks/shadowsocks-windows` 上游仓库。 + +如果没有任何完整配置的 Shadowsocks 服务器,依赖服务器的 DNS、PAC 以及启用 Windows 系统代理的操作会保持不可用;添加有效服务器后才会解锁。新的 DNSCrypt 配置首次使用时除 IPv6 外所有选项默认开启,但磁盘上已经保存的 DNSCrypt 设置在 schema migration 中不会被改写。Start on Boot、Administrator capture/UAC 切换和更新等耗时操作会显示明确的 busy progress。WinUI 导航与权限图标统一使用兼容 Windows 10 的 Segoe MDL2 Assets。 + +## 当前版本:5.2.31 + +5.2.31 已在 Administrator Mode 中实现 `System`、`Direct`、`Proxy`、`CustomDoh` 和 `DnsCrypt` 五种 DNS policy 的透明 DNS 拦截/路由,同时包含 DNSCrypt/DoH 管理(ODoH 保持关闭)、自动更新加固、开机启动重复实例保护以及当前 WinUI 3 界面。发布流程还会检查 .NET 安全基线、NuGet 漏洞审计、Windows 10 build 19041 兼容性以及最终 EXE 的版本元数据。 + +完整版本历史见 [更新日志](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Changelog)。 + +## 主要特性 + +- 原生 WinUI 3 桌面界面;产品 UI 不再依赖 WinForms/WPF。 +- unpackaged、self-contained、win-x64、single-file 发布。 +- 正式发布包中只包含 `Shadowsocks.exe`。 +- 用户配置和可写数据统一存放在 `%LOCALAPPDATA%\Shadowsocks`。 +- Clean Mode(EXE 文件名在 `.exe` 前以 `p` 结尾)把全部可写状态重定向到一次性的 `%TEMP%\Shadowsocks\Clean\...` 会话。 +- 内置 HTTP/1.1 proxy 与 HTTPS `CONNECT`;Privoxy/sysproxy 已移除。 +- 按应用设置 `Proxy`、`Direct`、`Block` 路由。 +- Administrator Mode 通过 WinDivert 对 TCP/UDP 进行透明捕获。 +- 自动 Game Mode 会在配置的游戏/程序运行期间暂停 Admin capture,退出后自动恢复。 +- SIP003 插件管理:内置 `xray-plugin`、`v2ray-plugin`、`qtun` 选项,并支持手动导入 ZIP/TAR.GZ;导入需要已知但尚未安装插件的 `ss://` 时会先提示安装;受信任的内置目录插件在未被使用时每天自动检查一次更新,也可手动检查。 +- UDP relay、二维码导入/导出、热键以及单一内嵌 CSV 本地化目录。 + +## 流量模式 + +只有两种可选流量模式: + +- **User Mode** — 不需要提权,也不会解包 NetworkService。路由仅作用于进入本地/系统代理的流量。 +- **Admin Mode** — 请求 UAC,将内嵌的 `Shadowsocks.NetworkService.exe` 按需释放到当前存储根目录,校验后提权启动,并通过 WinDivert 启用透明 TCP/UDP 捕获。 + +**Game Mode 是自动兼容状态,不是第三种流量模式。** 当选择 Admin Mode 且配置的游戏/程序启动时,WinDivert capture 会暂停;程序退出后自动恢复。 + +Traffic 页面会显示配置模式、运行模式、NetworkService、WinDivert、TCP/UDP capture 状态以及 redirect ports。 + +## 系统要求 + +- Windows 10 2004 / build 19041 或更高版本,或 Windows 11; +- x64 Windows; +- 从源码构建需要 .NET SDK 10.0.303 或更高版本(release build 使用 .NET 10.0.11 安全基线)。 + +正式发布版本为 self-contained,不要求用户另行安装 .NET Runtime。 + +## Solution 结构 + +- `Shadowsocks.Core` — 协议、加密、配置模型、PAC/GeoSite、路由模型、本地化和存储抽象。 +- `Shadowsocks.Windows` — 文件存储 bootstrap、WinINet/system proxy、开机启动、UAC/Admin capture、WinDivert runtime、热键和其他 Windows 集成。 +- `Shadowsocks.Windows.WinUI` — 独立的 WinUI shell/tray/QR/power 集成库,不依赖 Core/Windows ProjectReference。 +- `Shadowsocks.WinUI` — WinUI 3 应用 shell 和正式发布项目(`Shadowsocks.exe`)。 +- `Shadowsocks.NetworkService` — 内嵌到正式版本中的独立提权 WinDivert helper。 +- `Shadowsocks.UnitTests` — 不依赖 presentation layer 的 Core/Windows 测试。 + +架构说明见 [架构](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Architecture)。 + +## 构建 + +`Directory.Build.props` 是唯一的三段式产品版本源;`ApplicationInfo.Version`、manifest 和 release notes 都会与其进行一致性校验。 + +在 Windows 上使用 .NET SDK 10.0.303 或更高版本: + +```powershell +dotnet restore .\shadowsocks-reborn.sln -p:Platform=x64 -r win-x64 +dotnet build .\shadowsocks-reborn.sln -c Release -p:Platform=x64 -m:1 --no-restore +dotnet test .\Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj -c Release -p:Platform=x64 --no-build +``` + +构建正式 ZIP 与 SHA-256: + +```powershell +.\packaging\Validate-Repository.ps1 +.\packaging\Build-Release.ps1 +``` + +GitHub Release 的固定资源名为: + +```text +Shadowsocks-win-x64.zip +Shadowsocks-win-x64.zip.sha256 +``` + +最终 publish 目录与 ZIP 中只能有: + +```text +Shadowsocks.exe +``` + +## 存储与开机启动 + +普通模式把持久化状态存放在: + +```text +%LOCALAPPDATA%\Shadowsocks +``` + +配置文件为 `%LOCALAPPDATA%\Shadowsocks\settings.json`,并使用原子备份 `settings.backup.json`。插件安装在 `%LOCALAPPDATA%\Shadowsocks\Plugins`。内置目录插件会记录受信任的 GitHub release 来源,并可在未被使用时每天自动更新;如果当前 tag 与候选 tag 都能解析为版本,则更低的候选版本会被拒绝。手动导入的 ZIP/TAR.GZ 插件不会参与后台网络更新。本地化只使用内嵌在 `Shadowsocks.exe` 中的 `i18n.csv`。 + +将 EXE 名称改为在 `.exe` 前以 `p` 结尾即可进入 **Clean Mode**,例如 `Shadowsocksp.exe` 或 `Shadowsocks-cleanp.exe`。Clean Mode 会把 settings、cache、PAC、logs、插件、runtime/helper 工作目录统一放入 `%TEMP%\Shadowsocks\Clean\...`,退出时尽力清理。Clean Mode 不允许启用 Start with Windows。 + +普通模式下,Start with Windows 使用 `%LOCALAPPDATA%\Shadowsocks\Startup\Shadowsocks.exe` 的已校验稳定副本。启动时会迁移旧的 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 项、删除重复项;关闭开机启动时也会清理匹配的旧项。与 EXE identity 无关的进程级 single-instance guard 可阻止 startup copy 与原始 EXE 同时启动两个 controller 并争用同一端口。Windows Restart Manager 与 Start with Windows 保持互斥。两种机制都会持续用 `--start-visible` 或 `--start-hidden` 记录实际 UI 状态,因此重启/登录后会恢复关机前的状态:原先打开的窗口重新打开,原先仅在托盘中的实例仍保持在托盘。`WM_QUERYENDSESSION` 会在 Windows 关闭 HWND 之前保存状态,避免把系统关机误判成用户主动“关闭到托盘”。 + +详细规则见 [存储策略](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Storage-Policy)。 + +## 自动更新 + +下载资源先写入 `*.download`,文件句柄关闭后才在 Windows 上通过 `File.Move` 提升为正式临时文件。`.sha256` 支持两种格式:` Shadowsocks-win-x64.zip` 或单独的 64 位十六进制 SHA-256。 + +应用默认在启动后检查 GitHub Releases,并且只接受**严格高于当前已安装版本的数字版本**(`5.2.22 < 5.2.31`,因此 5.2.22 会被忽略;`5.2.32 > 5.2.31`,因此 5.2.32 可以更新)。同时要求存在准确命名的 ZIP 与 `.sha256`,并校验 SHA-256、ZIP 布局以及 payload FileVersion。新 EXE 在 `%TEMP%\Shadowsocks\Updates` 中 staging;如果当前进程来自 LocalAppData 的旧 startup copy,更新检查会使用“当前进程版本”和“记录的主 EXE FileVersion”两者中的较高版本,因此旧 startup copy 不能把更旧的 release 当成更新。UAC handoff 期间 staged updater 会再次校验自身 SHA-256;在真正替换主 EXE 前还会重新读取目标 FileVersion,并拒绝相同或更旧的 payload,随后才执行 rollback-safe 替换、启动新版本并清理临时 transaction。 + +## Embedded NetworkService + +Release build 会把 self-contained single-file 的 `Shadowsocks.NetworkService` 内嵌进 `Shadowsocks.exe`。 + +User Mode 不会解包 helper。Admin Mode 才会按需释放到当前 storage root,串行化 extraction,进行 SHA-256 校验,并通过 elevated control pipe 做版本握手。停止 broker 后会尽力删除 helper,旧 runtime 目录随后清理。 + +## WinDivert 验证 + +只有提权 capture 进程成功打开 WinDivert 后,Admin Mode 才会报告为 active。日志中会出现类似: + +```text +WinDivert capture confirmed (start): Admin capture active.; TCP redirect port=..., UDP redirect port=... +``` + +功能测试可给 `curl.exe` 配置 `Block`,临时关闭 Windows system proxy,然后执行: + +```cmd +curl.exe -4 --noproxy "*" https://example.com +``` + +在 User Mode 中直连请求应绕过 application routing;在 Admin Mode 中应被阻止。 + +## PAC、HTTP forwarding 与 DNS + +Local GeoSite + EasyList/ABP 路由现在**始终由托管 C# 实现**。Local PAC 文件只包含一个最小的 `FindProxyForURL` funnel,用于把流量送入 `ManagedHttpProxyService`;PAC 本身不再执行过滤规则。先应用 application rules,再由 `Shadowsocks.Routing.FilterEngine` 中不可变的 GeoSite + `user-rule.txt` snapshot 作出权威 `DIRECT`/`PROXY` 决策。历史 `abp.js`、compiled-PAC 兼容后端、后端选择器以及可执行的自定义 `abp.txt` 覆盖均已删除;设置架构升级时,已删除的属性会在规范化迁移过程中自动清理。**Online PAC 是独立模式:**只有用户显式启用时,所选外部 PAC 程序才保持权威,因为任意 PAC JavaScript 无法无损转换为 ABP/EasyList 规则。 在 Local 模式下,**PAC / GeoSite** 页面提供 **Open user-rule.txt**,用户规则优先于 GeoSite 默认规则,并在文件变化后自动应用。启用 Online PAC 时会隐藏 Local managed routing、user-rule 和 GeoSite 控件。 + +Administrator Mode 会把同一 managed rule set 发送给提权 NetworkService。没有显式 application rule 的普通 TCP 在 SYN 阶段标记为 `Deferred` 并反射到 transparent relay;relay 只读取连接开头的明文路由元数据,提取 HTTP `Host` 或 TLS ClientHello SNI,然后选择直连或 Shadowsocks SOCKS5。**不会解密 TLS、不会安装证书,也不会进行 MITM。** HTTP 可按 path/query 匹配;TLS/SNI 只能按 hostname 匹配。显式 application rule 仍具有最高优先级。UDP、TCP DNS/53 以及无法获得 Host/SNI 的协议继续使用确定性的 fallback;连续规则更新会合并,并仅通过已提权 broker 重启 active capture child。 + +`ManagedHttpProxyService` 支持 HTTP/1.1 与 HTTPS `CONNECT`。HTTPS 仅进行 byte tunnel,因此 TLS 内协商 HTTP/2 不需要本地 HTTP/2 parser。未实现 FTP gateway。 + +### DNS policy 合约 + +5.2.31 的配置/IPC 包含五种 DNS policy。Administrator Mode 已通过 `Shadowsocks.NetworkService` + WinDivert 对 UDP/TCP 53 的经典 DNS 实现透明路由: + +| Policy | 当前行为 | +| --- | --- | +| `System` | 保持捕获的经典 DNS 使用原始/系统 destination,不做 policy-specific redirect。 | +| `Direct` | 可保留原始 destination,或把 DNS 重定向到配置的主/备用 IPv4/IPv6 resolver;也可把所选 DNS endpoint 通过本地 Shadowsocks SOCKS5 路由。 | +| `Proxy` | 把捕获的经典 DNS 通过 Shadowsocks 转发。 | +| `CustomDoh` | 将捕获的 DNS wire message 桥接到指定 HTTPS DoH endpoint;DoH upstream 可独立选择是否通过 Shadowsocks。 | +| `DnsCrypt` | 重定向到动态分配的本地 `dnscrypt-proxy` listener;安全 runtime 不可用时采用 fail-closed。 | + +因此,**5.2.31 已实现透明的系统级 DNS interception/routing**,但必须使用 **Administrator Mode**。User Mode 仍可使用 Shadowsocks 管理的 DNS/DNSCrypt,但不会拦截任意系统 DNS。Game Mode 暂停 Admin capture 时也会暂停系统级 DNS 拦截。透明拦截范围为 UDP/TCP 53 的经典 DNS;应用内部的 DoH、DoT、DoQ 不会被通用拦截。 + +DNS 页面可以管理可选且经过签名校验的 `dnscrypt-proxy`。`Automatic` 根据 DNSSEC、no-log、无过滤、IPv4/IPv6 等约束从签名公共目录选择具体 DNSCrypt/DoH resolver 并固定到 `server_names`。解析器目录维护首先通过 **Shadowsocks 上的 Cloudflare DoH** 解析源主机名,并以 **Shadowsocks 上的 Google DoH** 作为备用;目录与 `.minisig` 也通过同一隧道下载,并使用固定的 DNSCrypt Minisign 公钥验证。验证成功后才把本地认证缓存交给 `dnscrypt-proxy`。目录 profile 与活动 runtime 都使用 `bootstrap_resolvers = []` 和 `ignore_system_dns = true`,目录 profile 还使用 `urls = []`,因此 dnscrypt-proxy 无法退回远程 source lookup 或系统/明文 bootstrap。`Manual` 支持 DNSCrypt/DoH resolver 以及协议、国家、地址族和隐私过滤;ODoH 保持关闭。 DNSCrypt 组件自身的 release metadata、ZIP 与 Minisign 下载也使用这条 Cloudflare→Google DoH-over-Shadowsocks 路径,包括 GitHub redirect 主机,因此安装/更新不会有意依赖 Windows resolver 来解析 `api.github.com` 或 release assets。 + +DNS 页面还提供 `Test DNSCrypt` 与 DNS privacy self-test。loopback listener 端口会确认同一数字端口可同时用于 UDP/TCP。DNSCrypt PID/port 变化会实时同步到 NetworkService。启动、重启、恢复或 runtime 故障期间采用 fail-closed,不会把已拦截 DNS 静默降级到 plaintext system DNS。需要 DNS/proxy rewrite 的分片 datagram 会整体丢弃,防止后续 fragment 绕过策略;direct fragment 仍保持 direct。 + +WinDivert 2.2.2 只从固定官方 release URL 下载,并在加载前校验 x64 DLL/driver 的固定 SHA-256。read-only FLOW observer 提供 endpoint PID ownership,IP Helper 作为 fallback。 + +首次启用 DNSCrypt 时,DNS 页面会持续显示带动画的安装/验证操作卡片,而不是只留下被禁用且没有进度提示的设置界面。 + +DNSCrypt Automatic 优先选择与当前 Shadowsocks server 国家匹配的兼容 resolver,无匹配时选择签名目录中的最佳兼容项。resolver 国家只根据 endpoint IP 的 GeoIP 推导,不使用名称/description。Manual resolver latency 异步测量,活动 resolver 在可用时使用 dnscrypt-proxy 实际 RTT。组件自动更新检查最多每 24 小时一次;首次安装始终需要用户显式操作。如果 DNSCrypt 自身通过 Shadowsocks 路由,则 Shadowsocks/forward-proxy endpoint 必须使用 IP literal,以避免 DNS bootstrap 递归。 + +## 文档 + +- [中文使用说明](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-User-Guide) +- [User guide (English)](https://github.com/SlimRG/shadowsocks-reborn/wiki/EN-User-Guide) +- [Руководство пользователя (RU)](https://github.com/SlimRG/shadowsocks-reborn/wiki/RU-User-Guide) + +三个 release-facing README 文件会镜像保留在主仓库;长篇文档统一维护在 GitHub Wiki。 + +- [架构](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Architecture) +- [存储策略](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Storage-Policy) +- [Windows 11 / WinUI 指南](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Windows-11-WinUI-Guide) +- [参与开发](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Contributing) +- [发布检查清单](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Release-Checklist) +- [安全](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Security) +- [更新日志](https://github.com/SlimRG/shadowsocks-reborn/wiki/ZH-Changelog) + +## 许可证 + +`shadowsocks-reborn` 使用 **GPL-3.0-or-later**。见 [许可证](LICENSE.txt) 与主仓库中的 [LICENSE.txt](https://github.com/SlimRG/shadowsocks-reborn/blob/main/LICENSE.txt)。第三方组件保留各自许可证,详情见 [第三方组件声明](THIRD-PARTY-NOTICES.txt)。 diff --git a/Shadowsocks.Core/ApplicationInfo.cs b/Shadowsocks.Core/ApplicationInfo.cs new file mode 100644 index 000000000..e5921c8f9 --- /dev/null +++ b/Shadowsocks.Core/ApplicationInfo.cs @@ -0,0 +1,14 @@ +namespace Shadowsocks.Core +{ + /// + /// Product metadata shared by Core, Windows infrastructure and UI. + /// Keep release versioning in one platform-neutral location. + /// + public static class ApplicationInfo + { + public const string Version = "5.2.31.0"; + public const string RepositoryUrl = "https://github.com/SlimRG/shadowsocks-reborn"; + public const string IssuesUrl = RepositoryUrl + "/issues"; + public const string ReleasesApiUrl = "https://api.github.com/repos/SlimRG/shadowsocks-reborn/releases"; + } +} diff --git a/shadowsocks-csharp/Controller/FileManager.cs b/Shadowsocks.Core/Controller/FileManager.cs old mode 100755 new mode 100644 similarity index 88% rename from shadowsocks-csharp/Controller/FileManager.cs rename to Shadowsocks.Core/Controller/FileManager.cs index ef30470d7..b2d5522b0 --- a/shadowsocks-csharp/Controller/FileManager.cs +++ b/Shadowsocks.Core/Controller/FileManager.cs @@ -1,68 +1,68 @@ -using NLog; -using System; -using System.IO; -using System.IO.Compression; -using System.Text; - -namespace Shadowsocks.Controller -{ - public static class FileManager - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public static bool ByteArrayToFile(string fileName, byte[] content) - { - try - { - using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) - fs.Write(content, 0, content.Length); - return true; - } - catch (Exception ex) - { - logger.Error(ex); - } - return false; - } - - public static void UncompressFile(string fileName, byte[] content) - { - // Because the uncompressed size of the file is unknown, - // we are using an arbitrary buffer size. - byte[] buffer = new byte[4096]; - int n; - - using(var fs = File.Create(fileName)) - using (var input = new GZipStream(new MemoryStream(content), - CompressionMode.Decompress, false)) - { - while ((n = input.Read(buffer, 0, buffer.Length)) > 0) - { - fs.Write(buffer, 0, n); - } - } - } - - public static string NonExclusiveReadAllText(string path) - { - return NonExclusiveReadAllText(path, Encoding.Default); - } - - public static string NonExclusiveReadAllText(string path, Encoding encoding) - { - try - { - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - using (var sr = new StreamReader(fs, encoding)) - { - return sr.ReadToEnd(); - } - } - catch (Exception ex) - { - logger.Error(ex); - throw ex; - } - } - } -} +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using NLog; + +namespace Shadowsocks.Controller +{ + public static class FileManager + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public static bool ByteArrayToFile(string fileName, byte[] content) + { + try + { + using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) + fs.Write(content, 0, content.Length); + return true; + } + catch (Exception ex) + { + logger.Error(ex); + } + return false; + } + + public static void UncompressFile(string fileName, byte[] content) + { + // Because the uncompressed size of the file is unknown, + // we are using an arbitrary buffer size. + byte[] buffer = new byte[4096]; + int n; + + using (var fs = File.Create(fileName)) + using (var input = new GZipStream(new MemoryStream(content), + CompressionMode.Decompress, false)) + { + while ((n = input.Read(buffer, 0, buffer.Length)) > 0) + { + fs.Write(buffer, 0, n); + } + } + } + + public static string NonExclusiveReadAllText(string path) + { + return NonExclusiveReadAllText(path, Encoding.Default); + } + + public static string NonExclusiveReadAllText(string path, Encoding encoding) + { + try + { + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var sr = new StreamReader(fs, encoding)) + { + return sr.ReadToEnd(); + } + } + catch (Exception ex) + { + logger.Error(ex); + throw; + } + } + } +} diff --git a/Shadowsocks.Core/Controller/I18N.cs b/Shadowsocks.Core/Controller/I18N.cs new file mode 100644 index 000000000..31e7fcfa7 --- /dev/null +++ b/Shadowsocks.Core/Controller/I18N.cs @@ -0,0 +1,22 @@ +using System; +using Shadowsocks.Localization; + +namespace Shadowsocks.Controller +{ + public static class I18N + { + + private static ILocalizationService _service; + + public static ILocalizationService Service => _service ??= CsvLocalizationService.CreateDefault(); + + public static void Configure(ILocalizationService service) + { + ArgumentNullException.ThrowIfNull(service); + _service = service; + } + + public static string GetString(string key, params object[] args) + => Service.Format(key, args); + } +} diff --git a/shadowsocks-csharp/Controller/LoggerExtension.cs b/Shadowsocks.Core/Controller/LoggerExtension.cs similarity index 70% rename from shadowsocks-csharp/Controller/LoggerExtension.cs rename to Shadowsocks.Core/Controller/LoggerExtension.cs index bd9b59252..5106e35a1 100644 --- a/shadowsocks-csharp/Controller/LoggerExtension.cs +++ b/Shadowsocks.Core/Controller/LoggerExtension.cs @@ -1,119 +1,107 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Net.Sockets; -using System.Net; -using System.Diagnostics; -using System.Text; -using Shadowsocks.Util.SystemProxy; - -namespace NLog -{ - public static class LoggerExtension - { - public static void Dump(this Logger logger, string tag, byte[] arr, int length) - { - if (logger.IsTraceEnabled) - { - var sb = new StringBuilder($"{Environment.NewLine}{tag}: "); - for (int i = 0; i < length - 1; i++) - { - sb.Append($"0x{arr[i]:X2}, "); - } - sb.Append($"0x{arr[length - 1]:X2}"); - sb.Append(Environment.NewLine); - logger.Trace(sb.ToString()); - } - } - - public static void Debug(this Logger logger, EndPoint local, EndPoint remote, int len, string header = null, string tailer = null) - { - if (logger.IsDebugEnabled) - { - if (header == null && tailer == null) - logger.Debug($"{local} => {remote} (size={len})"); - else if (header == null && tailer != null) - logger.Debug($"{local} => {remote} (size={len}), {tailer}"); - else if (header != null && tailer == null) - logger.Debug($"{header}: {local} => {remote} (size={len})"); - else - logger.Debug($"{header}: {local} => {remote} (size={len}), {tailer}"); - } - } - - public static void Debug(this Logger logger, Socket sock, int len, string header = null, string tailer = null) - { - if (logger.IsDebugEnabled) - { - logger.Debug(sock.LocalEndPoint, sock.RemoteEndPoint, len, header, tailer); - } - } - - public static void LogUsefulException(this Logger logger, Exception e) - { - // just log useful exceptions, not all of them - if (e is SocketException) - { - SocketException se = (SocketException)e; - if (se.SocketErrorCode == SocketError.ConnectionAborted) - { - // closed by browser when sending - // normally happens when download is canceled or a tab is closed before page is loaded - } - else if (se.SocketErrorCode == SocketError.ConnectionReset) - { - // received rst - } - else if (se.SocketErrorCode == SocketError.NotConnected) - { - // The application tried to send or receive data, and the System.Net.Sockets.Socket is not connected. - } - else if (se.SocketErrorCode == SocketError.HostUnreachable) - { - // There is no network route to the specified host. - } - else if (se.SocketErrorCode == SocketError.TimedOut) - { - // The connection attempt timed out, or the connected host has failed to respond. - } - else - { - logger.Warn(e); - } - } - else if (e is ObjectDisposedException) - { - } - else if (e is Win32Exception) - { - var ex = (Win32Exception)e; - - // Win32Exception (0x80004005): A 32 bit processes cannot access modules of a 64 bit process. - if ((uint)ex.ErrorCode != 0x80004005) - { - logger.Warn(e); - } - } - else if (e is ProxyException) - { - var ex = (ProxyException)e; - switch (ex.Type) - { - case ProxyExceptionType.FailToRun: - case ProxyExceptionType.QueryReturnMalformed: - case ProxyExceptionType.SysproxyExitError: - logger.Error($"sysproxy - {ex.Type.ToString()}:{ex.Message}"); - break; - case ProxyExceptionType.QueryReturnEmpty: - case ProxyExceptionType.Unspecific: - logger.Error($"sysproxy - {ex.Type.ToString()}"); - break; - } - } - else - { - logger.Warn(e); - } - } - } -} +using System; +using System.ComponentModel; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace NLog +{ + public static class LoggerExtension + { + public static void Dump(this Logger logger, string tag, byte[] arr, int length) + { + if (logger.IsTraceEnabled) + { + ArgumentNullException.ThrowIfNull(arr); + if ((uint)length > (uint)arr.Length || length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + var sb = new StringBuilder(); + sb.Append(CultureInfo.InvariantCulture, $"{Environment.NewLine}{tag}: "); + for (int i = 0; i < length - 1; i++) + { + sb.Append(CultureInfo.InvariantCulture, $"0x{arr[i]:X2}, "); + } + sb.Append(CultureInfo.InvariantCulture, $"0x{arr[length - 1]:X2}"); + sb.Append(Environment.NewLine); + logger.Trace(sb.ToString()); + } + } + + public static void Debug(this Logger logger, EndPoint local, EndPoint remote, int len, string header = null, string tailer = null) + { + if (logger.IsDebugEnabled) + { + if (header == null && tailer == null) + logger.Debug($"{local} => {remote} (size={len})"); + else if (header == null && tailer != null) + logger.Debug($"{local} => {remote} (size={len}), {tailer}"); + else if (header != null && tailer == null) + logger.Debug($"{header}: {local} => {remote} (size={len})"); + else + logger.Debug($"{header}: {local} => {remote} (size={len}), {tailer}"); + } + } + + public static void Debug(this Logger logger, Socket sock, int len, string header = null, string tailer = null) + { + if (logger.IsDebugEnabled) + { + logger.Debug(sock.LocalEndPoint, sock.RemoteEndPoint, len, header, tailer); + } + } + + public static void LogUsefulException(this Logger logger, Exception e) + { + // just log useful exceptions, not all of them + if (e is SocketException) + { + SocketException se = (SocketException)e; + if (se.SocketErrorCode == SocketError.ConnectionAborted) + { + // closed by browser when sending + // normally happens when download is canceled or a tab is closed before page is loaded + } + else if (se.SocketErrorCode == SocketError.ConnectionReset) + { + // received rst + } + else if (se.SocketErrorCode == SocketError.NotConnected) + { + // The application tried to send or receive data, and the System.Net.Sockets.Socket is not connected. + } + else if (se.SocketErrorCode == SocketError.HostUnreachable) + { + // There is no network route to the specified host. + } + else if (se.SocketErrorCode == SocketError.TimedOut) + { + // The connection attempt timed out, or the connected host has failed to respond. + } + else + { + logger.Warn(e); + } + } + else if (e is ObjectDisposedException) + { + } + else if (e is Win32Exception ex) + { + // Never suppress Win32Exception by HRESULT. Win32Exception.ErrorCode can be + // the generic E_FAIL (0x80004005) for unrelated native failures, which used + // to hide real errors such as failed WinINet proxy updates. + logger.Warn( + e, + $"Win32 API failure (NativeErrorCode={ex.NativeErrorCode}, HRESULT=0x{unchecked((uint)ex.ErrorCode):X8})."); + } + else + { + logger.Warn(e); + } + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/GeositeUpdater.cs b/Shadowsocks.Core/Controller/Service/GeositeUpdater.cs new file mode 100644 index 000000000..e2ea26b99 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/GeositeUpdater.cs @@ -0,0 +1,621 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Model; +using Shadowsocks.Core.Storage; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller.Service +{ + public class GeositeResultEventArgs(bool success) : EventArgs + { + public bool Success { get; } = success; + } + + public static class GeositeUpdater + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Lock databaseLock = new(); + private static readonly SemaphoreSlim updateLock = new(1, 1); + + public static event EventHandler UpdateCompleted; + public static event ErrorEventHandler Error; + + public const string DefaultSourceUrl = "https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"; + + private static readonly string CacheDirectory = AppStoragePaths.GeositeCacheDirectory; + private static readonly string AppliedSourcesPath = Path.Combine(CacheDirectory, "active-sources.sha256"); + + private static List configuredSources = [DefaultSourceUrl]; + private static string configuredSourcesFingerprint = GetSourcesFingerprint([DefaultSourceUrl]); + private static HashSet sourcesNeedingRefresh = new(StringComparer.OrdinalIgnoreCase); + + public static readonly Dictionary> Geosites = + new(StringComparer.OrdinalIgnoreCase); + + public static bool IsDatabaseAvailable + { + get + { + lock (databaseLock) + { + return Geosites.Count > 0; + } + } + } + + public static bool NeedsRefresh + { + get + { + lock (databaseLock) + { + return configuredSources.Count == 0 || sourcesNeedingRefresh.Count > 0; + } + } + } + + public static bool IsSourceSetDirty + { + get + { + string expected; + lock (databaseLock) + { + expected = configuredSourcesFingerprint; + } + + try + { + return !File.Exists(AppliedSourcesPath) || + !string.Equals(File.ReadAllText(AppliedSourcesPath).Trim(), expected, StringComparison.OrdinalIgnoreCase); + } + catch + { + return true; + } + } + } + + /// + /// Switch the in-memory GeoSite view to the caches belonging to the configured URLs. + /// No network traffic is performed here. All databases are merged by group name. + /// + public static void ConfigureSources(IEnumerable sources) + { + List normalized = NormalizeSources(sources); + Directory.CreateDirectory(CacheDirectory); + + List>> databases = []; + HashSet needsRefresh = new(StringComparer.OrdinalIgnoreCase); + foreach (string source in normalized) + { + string cachePath = GetCachePath(source); + if (!File.Exists(cachePath)) + { + needsRefresh.Add(source); + continue; + } + + try + { + databases.Add(ParseGeositeList(File.ReadAllBytes(cachePath))); + logger.Info($"Loaded cached GeoSite source: {source}"); + } + catch (Exception ex) + { + needsRefresh.Add(source); + logger.Warn(ex, $"Cached GeoSite source is invalid and will be refreshed when Local PAC needs it: {source}"); + } + } + + lock (databaseLock) + { + configuredSources = normalized; + configuredSourcesFingerprint = GetSourcesFingerprint(normalized); + sourcesNeedingRefresh = needsRefresh; + ReplaceGeositesNoLock(MergeDatabases(databases)); + } + } + + private static List NormalizeSources(IEnumerable sources) + { + List normalized = (sources ?? []) + .Select(source => source?.Trim()) + .Where(source => !string.IsNullOrWhiteSpace(source)) + .Where(source => Uri.TryCreate(source, UriKind.Absolute, out Uri uri) && + (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (normalized.Count == 0) + { + normalized.Add(DefaultSourceUrl); + } + + return normalized; + } + + private static Dictionary> ParseGeositeList(byte[] database) + { + if (database == null || database.Length == 0) + { + throw new InvalidDataException("GeoSite database is empty."); + } + + GeositeList list = GeositeList.Parser.ParseFrom(database); + Dictionary> parsed = new(StringComparer.OrdinalIgnoreCase); + foreach (Geosite item in list.Entries) + { + if (string.IsNullOrWhiteSpace(item.GroupName)) + { + continue; + } + + parsed[item.GroupName.ToLowerInvariant()] = item.Domains.ToList(); + } + + if (parsed.Count == 0) + { + throw new InvalidDataException("GeoSite database contains no groups."); + } + + return parsed; + } + + private static Dictionary> MergeDatabases( + IEnumerable>> databases) + { + Dictionary> merged = new(StringComparer.OrdinalIgnoreCase); + foreach (Dictionary> database in databases) + { + foreach (KeyValuePair> group in database) + { + if (!merged.TryGetValue(group.Key, out IList domains)) + { + domains = []; + merged[group.Key] = domains; + } + + foreach (DomainObject domain in group.Value) + { + domains.Add(domain); + } + } + } + return merged; + } + + private static void ReplaceGeositesNoLock(Dictionary> database) + { + Geosites.Clear(); + foreach (KeyValuePair> item in database) + { + Geosites[item.Key] = item.Value; + } + } + + private static string GetCachePath(string sourceUrl) + { + byte[] sourceHash = SHA256.HashData(Encoding.UTF8.GetBytes(sourceUrl)); + return Path.Combine(CacheDirectory, Convert.ToHexString(sourceHash) + ".dat"); + } + + private static string GetSourcesFingerprint(IEnumerable sources) + { + string canonical = string.Join("\n", (sources ?? []) + .Select(source => source?.Trim() ?? "")); + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))); + } + + private static void MarkCurrentSourceSetApplied() + { + string fingerprint; + lock (databaseLock) + { + fingerprint = configuredSourcesFingerprint; + } + + try + { + Directory.CreateDirectory(CacheDirectory); + File.WriteAllText(AppliedSourcesPath, fingerprint, Encoding.ASCII); + } + catch (Exception ex) + { + logger.Warn(ex, "Could not persist the active GeoSite source fingerprint."); + } + } + + public static string GetChecksumUrl(string sourceUrl) + { + Uri uri = new(sourceUrl, UriKind.Absolute); + UriBuilder builder = new(uri) + { + Path = uri.AbsolutePath + ".sha256sum" + }; + return builder.Uri.AbsoluteUri; + } + + public static void ResetEvent() + { + UpdateCompleted = null; + Error = null; + } + + /// + /// Download/update every configured GeoSite source through the already running + /// local Shadowsocks connection, merge them, then regenerate Local PAC. + /// A missing/unreachable .sha256sum is deliberately non-fatal. + /// + public static async Task UpdatePACFromGeosite(Configuration config, bool raiseEvents = true) + { + ArgumentNullException.ThrowIfNull(config); + if (config.useOnlinePac) + { + logger.Debug("Skipping GeoSite update because Online PAC is enabled."); + return false; + } + + List sources = NormalizeSources(config.geositeUrls); + bool blacklist = config.geositePreferDirect; + + await updateLock.WaitAsync(); + try + { + using HttpClient httpClient = LocalProxyHttpClient.Create(config); + List>> loadedDatabases = []; + List failedSources = []; + + foreach (string source in sources) + { + try + { + loadedDatabases.Add(await LoadOrRefreshSourceAsync(httpClient, source)); + } + catch (Exception ex) + { + failedSources.Add(source); + logger.Warn(ex, $"GeoSite source could not be loaded: {source}"); + } + } + + if (loadedDatabases.Count == 0) + { + throw new InvalidOperationException( + "None of the configured GeoSite sources could be loaded. " + + string.Join(", ", failedSources)); + } + + Dictionary> merged = MergeDatabases(loadedDatabases); + lock (databaseLock) + { + configuredSources = sources; + configuredSourcesFingerprint = GetSourcesFingerprint(sources); + sourcesNeedingRefresh = new(failedSources, StringComparer.OrdinalIgnoreCase); + ReplaceGeositesNoLock(merged); + } + + LogInvalidConfiguredGroups(config); + bool pacFileChanged = MergeAndWritePACFile(); + + if (failedSources.Count > 0) + { + logger.Warn("Local PAC was regenerated from available GeoSite sources; unavailable sources: " + + string.Join(", ", failedSources)); + } + + if (raiseEvents) + { + UpdateCompleted?.Invoke(null, new(pacFileChanged)); + } + return pacFileChanged; + } + catch (Exception ex) + { + if (raiseEvents) + { + Error?.Invoke(null, new(ex)); + return false; + } + throw; + } + finally + { + updateLock.Release(); + } + } + + private static async Task>> LoadOrRefreshSourceAsync( + HttpClient httpClient, + string sourceUrl) + { + Directory.CreateDirectory(CacheDirectory); + string cachePath = GetCachePath(sourceUrl); + string expectedHash = await TryGetChecksumAsync(httpClient, sourceUrl); + + if (!string.IsNullOrEmpty(expectedHash) && File.Exists(cachePath)) + { + try + { + byte[] cachedBytes = await File.ReadAllBytesAsync(cachePath); + string cachedHash = Convert.ToHexString(SHA256.HashData(cachedBytes)); + if (string.Equals(expectedHash, cachedHash, StringComparison.OrdinalIgnoreCase)) + { + logger.Info($"GeoSite source is up to date: {sourceUrl}"); + return ParseGeositeList(cachedBytes); + } + } + catch (Exception ex) + { + logger.Warn(ex, $"Cached GeoSite source could not be reused: {sourceUrl}"); + } + } + + try + { + logger.Info($"Downloading GeoSite through Shadowsocks: {sourceUrl}"); + byte[] downloadedBytes = await httpClient.GetByteArrayAsync(sourceUrl); + if (downloadedBytes.Length == 0) + { + throw new InvalidDataException("Downloaded GeoSite database is empty."); + } + + if (!string.IsNullOrEmpty(expectedHash)) + { + string downloadedHash = Convert.ToHexString(SHA256.HashData(downloadedBytes)); + if (!string.Equals(expectedHash, downloadedHash, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException($"GeoSite SHA256 mismatch for {sourceUrl}."); + } + } + + Dictionary> parsed = ParseGeositeList(downloadedBytes); + string tempPath = cachePath + ".new"; + await File.WriteAllBytesAsync(tempPath, downloadedBytes); + File.Move(tempPath, cachePath, true); + return parsed; + } + catch (Exception downloadException) + { + // The last known-good source cache is more useful than failing Local PAC + // merely because a mirror is temporarily offline or its checksum changed. + if (File.Exists(cachePath)) + { + try + { + byte[] cachedBytes = await File.ReadAllBytesAsync(cachePath); + Dictionary> parsed = ParseGeositeList(cachedBytes); + logger.Warn(downloadException, + $"GeoSite refresh failed; using last known-good cache for {sourceUrl}."); + return parsed; + } + catch (Exception cacheException) + { + throw new AggregateException( + $"GeoSite source and its cache are both unusable: {sourceUrl}", + downloadException, + cacheException); + } + } + + throw; + } + } + + private static async Task TryGetChecksumAsync(HttpClient httpClient, string sourceUrl) + { + string checksumUrl = GetChecksumUrl(sourceUrl); + try + { + using HttpResponseMessage response = await httpClient.GetAsync(checksumUrl); + if (!response.IsSuccessStatusCode) + { + logger.Info($"GeoSite checksum is unavailable ({(int)response.StatusCode}); continuing without it: {checksumUrl}"); + return null; + } + + string content = await response.Content.ReadAsStringAsync(); + Match match = Regex.Match(content ?? string.Empty, @"(?i)\b[0-9a-f]{64}\b"); + if (!match.Success) + { + logger.Info($"GeoSite checksum response has no SHA256 value; continuing without it: {checksumUrl}"); + return null; + } + + string checksum = match.Value.ToUpperInvariant(); + logger.Info($"GeoSite SHA256 for {sourceUrl}: {checksum}"); + return checksum; + } + catch (Exception ex) + { + logger.Warn(ex, $"GeoSite checksum could not be fetched; continuing without verification: {checksumUrl}"); + return null; + } + } + + private static void LogInvalidConfiguredGroups(Configuration config) + { + foreach (string group in (config.geositeDirectGroups ?? []) + .Concat(config.geositeProxiedGroups ?? []) + .Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!CheckGeositeGroup(group)) + { + logger.Warn($"Configured GeoSite group is absent from the currently merged sources and will be skipped: {group}"); + } + } + } + + /// + /// Merge and write pac.txt from the cached GeoSite database. + /// + public static bool MergeAndWritePACFile() + { + if (!IsDatabaseAvailable) + { + throw new InvalidOperationException("GeoSite database is not installed. Enable Local PAC and wait for the database download to complete."); + } + + string pacContent = PACDaemon.GetManagedFunnelPac(); + if (File.Exists(PACDaemon.PacFile)) + { + string original = FileManager.NonExclusiveReadAllText(PACDaemon.PacFile, Encoding.UTF8); + if (original == pacContent) + { + MarkCurrentSourceSetApplied(); + return false; + } + } + + File.WriteAllText(PACDaemon.PacFile, pacContent, Encoding.UTF8); + MarkCurrentSourceSetApplied(); + return true; + } + + /// + /// Checks if the specified group exists in the currently merged GeoSite databases. + /// + public static bool CheckGeositeGroup(string group) + { + if (!IsDatabaseAvailable || !SeparateAttributeFromGroupName(group, out string groupName, out _)) + { + return false; + } + + lock (databaseLock) + { + return Geosites.ContainsKey(groupName); + } + } + + private static bool SeparateAttributeFromGroupName(string group, out string groupName, out string attribute) + { + string[] splitGroupAttributeList = (group ?? string.Empty).Split('@'); + if (splitGroupAttributeList.Length == 1) + { + groupName = splitGroupAttributeList[0]; + attribute = ""; + } + else if (splitGroupAttributeList.Length == 2) + { + groupName = splitGroupAttributeList[0]; + attribute = splitGroupAttributeList[1]; + } + else + { + groupName = ""; + attribute = ""; + return false; + } + return !string.IsNullOrWhiteSpace(groupName); + } + + /// + /// Builds the ABP-compatible network rule set consumed by the managed C# routing snapshot. + /// Returning a copy keeps the mutable GeoSite + /// database behind its existing lock while allowing the compiled FilterEngine to be + /// immutable and lock-free during request evaluation. + /// + public static IReadOnlyList BuildManagedFilterRules( + List directGroups, + List proxiedGroups, + bool blacklist) + => GenerateRules(directGroups ?? [], proxiedGroups ?? [], blacklist).AsReadOnly(); + + private static List GenerateRules(List directGroups, List proxiedGroups, bool blacklist) + { + List ruleLines; + if (blacklist) + { + ruleLines = GenerateBlockingRules(proxiedGroups); + ruleLines.AddRange(GenerateExceptionRules(directGroups)); + } + else + { + ruleLines = ["/.*/"]; + ruleLines.AddRange(GenerateExceptionRules(directGroups)); + } + return ruleLines.Distinct(StringComparer.Ordinal).ToList(); + } + + private static List GenerateBlockingRules(List groups) + { + List ruleLines = []; + foreach (string group in groups ?? []) + { + if (!SeparateAttributeFromGroupName(group, out string groupName, out string attribute)) + { + continue; + } + + IList domainObjects; + lock (databaseLock) + { + if (!Geosites.TryGetValue(groupName, out domainObjects)) + { + continue; + } + domainObjects = domainObjects.ToList(); + } + + if (!string.IsNullOrEmpty(attribute)) + { + DomainObject.Types.Attribute attributeObject = new() + { + Key = attribute, + BoolValue = true + }; + foreach (DomainObject domainObject in domainObjects) + { + if (domainObject.Attribute.Contains(attributeObject)) + { + AddDomainRule(ruleLines, domainObject); + } + } + } + else + { + foreach (DomainObject domainObject in domainObjects) + { + AddDomainRule(ruleLines, domainObject); + } + } + } + return ruleLines.Distinct(StringComparer.Ordinal).ToList(); + } + + private static void AddDomainRule(List ruleLines, DomainObject domainObject) + { + switch (domainObject.Type) + { + case DomainObject.Types.Type.Plain: + ruleLines.Add(domainObject.Value); + break; + case DomainObject.Types.Type.Regex: + ruleLines.Add($"/{domainObject.Value}/"); + break; + case DomainObject.Types.Type.Domain: + ruleLines.Add($"||{domainObject.Value}"); + break; + case DomainObject.Types.Type.Full: + ruleLines.Add($"|http://{domainObject.Value}"); + ruleLines.Add($"|https://{domainObject.Value}"); + break; + } + } + + private static List GenerateExceptionRules(List groups) + => GenerateBlockingRules(groups) + .Select(r => $"@@{r}") + .ToList(); + } +} diff --git a/Shadowsocks.Core/Controller/Service/Listener.cs b/Shadowsocks.Core/Controller/Service/Listener.cs new file mode 100644 index 000000000..22475052e --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/Listener.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using NLog; +using Shadowsocks.Core; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller +{ + public sealed class Listener : IDisposable + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public interface IService + { + bool Handle(byte[] firstPacket, int length, Socket socket, object state); + + void Shutdown(); + } + + public abstract class Service : IService + { + public abstract bool Handle(byte[] firstPacket, int length, Socket socket, object state); + + public virtual void Shutdown() { } + } + + public sealed class UDPState + { + public UDPState(Socket socket) + { + Socket = socket; + Buffer = new byte[4096]; + RemoteEndPoint = new IPEndPoint( + socket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, + 0); + } + + public Socket Socket { get; } + public byte[] Buffer { get; } + public EndPoint RemoteEndPoint { get; set; } + } + + private sealed record ReceiveState(Socket Socket, byte[] Buffer); + + private readonly List _services; + private Configuration _config; + private bool _shareOverLAN; + private Socket _tcpSocket; + private Socket _udpSocket; + private bool _disposed; + + public Listener(List services) + { + ArgumentNullException.ThrowIfNull(services); + _services = services; + } + + private static bool CheckIfPortInUse(int port) + { + IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); + return ipProperties.GetActiveTcpListeners().Any(endPoint => endPoint.Port == port); + } + + public void Start(Configuration config) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(config); + + _config = config; + _shareOverLAN = config.shareOverLan; + + if (CheckIfPortInUse(_config.localPort)) + { + throw new InvalidOperationException(I18N.GetString("Port {0} already in use", _config.localPort)); + } + + try + { + AddressFamily addressFamily = config.isIPv6Enabled + ? AddressFamily.InterNetworkV6 + : AddressFamily.InterNetwork; + _tcpSocket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + _udpSocket = new Socket(addressFamily, SocketType.Dgram, ProtocolType.Udp); + _tcpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + IPAddress bindAddress = _shareOverLAN + ? (config.isIPv6Enabled ? IPAddress.IPv6Any : IPAddress.Any) + : (config.isIPv6Enabled ? IPAddress.IPv6Loopback : IPAddress.Loopback); + IPEndPoint localEndPoint = new(bindAddress, _config.localPort); + + _tcpSocket.Bind(localEndPoint); + _udpSocket.Bind(localEndPoint); + _tcpSocket.Listen(1024); + + logger.Info($"Shadowsocks started ({ApplicationInfo.Version})"); + logger.Debug(Encryption.EncryptorFactory.DumpRegisteredEncryptor()); + _tcpSocket.BeginAccept(AcceptCallback, _tcpSocket); + + UDPState udpState = new(_udpSocket); + EndPoint remoteEndPoint = udpState.RemoteEndPoint; + _udpSocket.BeginReceiveFrom( + udpState.Buffer, 0, udpState.Buffer.Length, SocketFlags.None, ref remoteEndPoint, RecvFromCallback, udpState); + udpState.RemoteEndPoint = remoteEndPoint; + } + catch + { + Stop(); + throw; + } + } + + public void Stop() + { + Socket tcpSocket = _tcpSocket; + _tcpSocket = null; + tcpSocket?.Dispose(); + + Socket udpSocket = _udpSocket; + _udpSocket = null; + udpSocket?.Dispose(); + + foreach (IService service in _services) + { + service.Shutdown(); + } + } + + public void RecvFromCallback(IAsyncResult ar) + { + if (ar.AsyncState is not UDPState state) + { + return; + } + + Socket socket = state.Socket; + try + { + EndPoint remoteEndPoint = state.RemoteEndPoint; + int bytesRead = socket.EndReceiveFrom(ar, ref remoteEndPoint); + state.RemoteEndPoint = remoteEndPoint; + foreach (IService service in _services) + { + if (service.Handle(state.Buffer, bytesRead, socket, state)) + { + break; + } + } + } + catch (ObjectDisposedException) { } + catch (SocketException e) when (IsExpectedSocketShutdown(e)) { } + catch (Exception ex) + { + logger.Debug(ex); + } + finally + { + if (ReferenceEquals(socket, _udpSocket)) + { + try + { + EndPoint remoteEndPoint = state.RemoteEndPoint; + socket.BeginReceiveFrom( + state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref remoteEndPoint, RecvFromCallback, state); + state.RemoteEndPoint = remoteEndPoint; + } + catch (ObjectDisposedException) { } + catch (SocketException e) when (IsExpectedSocketShutdown(e)) { } + catch (Exception ex) { logger.Debug(ex); } + } + } + } + + public void AcceptCallback(IAsyncResult ar) + { + if (ar.AsyncState is not Socket listener) + { + return; + } + + Socket connection = null; + try + { + connection = listener.EndAccept(ar); + byte[] buffer = new byte[4096]; + var state = new ReceiveState(connection, buffer); + connection.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, state); + connection = null; // ownership transferred to ReceiveCallback + } + catch (ObjectDisposedException) { } + catch (SocketException e) when (IsExpectedSocketShutdown(e)) { } + catch (Exception e) + { + logger.LogUsefulException(e); + } + finally + { + connection?.Dispose(); + if (ReferenceEquals(listener, _tcpSocket)) + { + try { listener.BeginAccept(AcceptCallback, listener); } + catch (ObjectDisposedException) { } + catch (SocketException e) when (IsExpectedSocketShutdown(e)) { } + catch (Exception e) { logger.LogUsefulException(e); } + } + } + } + + private static bool IsExpectedSocketShutdown(SocketException exception) => + exception.SocketErrorCode is SocketError.OperationAborted or SocketError.Interrupted; + + private void ReceiveCallback(IAsyncResult ar) + { + if (ar.AsyncState is not ReceiveState state) + { + return; + } + + Socket connection = state.Socket; + try + { + int bytesRead = connection.EndReceive(ar); + if (bytesRead > 0) + { + foreach (IService service in _services) + { + if (service.Handle(state.Buffer, bytesRead, connection, null)) + { + return; + } + } + } + + connection.Dispose(); + } + catch (Exception e) + { + logger.LogUsefulException(e); + connection.Dispose(); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + Stop(); + _disposed = true; + GC.SuppressFinalize(this); + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/LocalProxyHttpClient.cs b/Shadowsocks.Core/Controller/Service/LocalProxyHttpClient.cs new file mode 100644 index 000000000..32fe1c4db --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/LocalProxyHttpClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Net; +using System.Net.Http; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Creates HTTP clients that explicitly use the local Shadowsocks HTTP endpoint. + /// The request enters the mixed local listener, is handled by the managed HTTP proxy + /// and then leaves through the currently selected Shadowsocks server. + /// + internal static class LocalProxyHttpClient + { + public static HttpClient Create(Configuration config) + { + ArgumentNullException.ThrowIfNull(config); + + var handler = new HttpClientHandler + { + Proxy = new WebProxy($"http://{config.LocalHost}:{config.localPort}"), + UseProxy = true, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, + }; + + var client = new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(30), + }; + + if (!string.IsNullOrWhiteSpace(config.userAgentString)) + client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", config.userAgentString); + + return client; + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/ManagedRoutingSnapshotBuilder.cs b/Shadowsocks.Core/Controller/Service/ManagedRoutingSnapshotBuilder.cs new file mode 100644 index 000000000..3e6e58863 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/ManagedRoutingSnapshotBuilder.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Shadowsocks.Model; +using Shadowsocks.Routing; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Builds one immutable managed-routing snapshot from the same Local PAC inputs used by + /// GeositeUpdater. Network/download work is intentionally excluded: callers publish a + /// bootstrap proxy-all snapshot until GeoSite has a complete local database, then rebuild. + /// + public static class ManagedRoutingSnapshotBuilder + { + public static ManagedRoutingSnapshot Build(Configuration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + if (!configuration.Enabled) + { + return ManagedRoutingSnapshot.Disabled("system proxy is disabled"); + } + + if (configuration.global) + { + return ManagedRoutingSnapshot.Disabled("global mode already routes every request through Shadowsocks"); + } + + if (configuration.useOnlinePac) + { + // An arbitrary downloaded PAC program cannot be losslessly reduced to ABP + // network filters. Keep the PAC decision authoritative in Online PAC mode. + return ManagedRoutingSnapshot.Disabled("online PAC is the selected external routing mode"); + } + + IReadOnlyList userRules = ReadUserRules(); + if (!GeositeUpdater.IsDatabaseAvailable || GeositeUpdater.NeedsRefresh || GeositeUpdater.IsSourceSetDirty) + { + IReadOnlyList bootstrapRules = new[] { "/.*/" }; + FilterEngine bootstrap = FilterEngine.Compile( + bootstrapRules, + Array.Empty(), + out FilterCompilationReport bootstrapReport); + return ManagedRoutingSnapshot.ProxyAllBootstrap( + bootstrap, + bootstrapReport, + "GeoSite is not ready; matching Local PAC proxy-all bootstrap", + bootstrapRules, + Array.Empty()); + } + + IReadOnlyList defaultRules = GeositeUpdater.BuildManagedFilterRules( + configuration.geositeDirectGroups, + configuration.geositeProxiedGroups, + configuration.geositePreferDirect); + FilterEngine engine = FilterEngine.Compile(defaultRules, userRules, out FilterCompilationReport report); + return ManagedRoutingSnapshot.LocalRules( + engine, + report, + "GeoSite + user rules", + defaultRules, + userRules); + } + + private static IReadOnlyList ReadUserRules() + { + try + { + if (!File.Exists(PACDaemon.UserRuleFile)) + { + return FilterEngine.ParseRuleLines(Shadowsocks.Core.EmbeddedResources.UserRule); + } + + string content = Shadowsocks.Controller.FileManager.NonExclusiveReadAllText(PACDaemon.UserRuleFile, Encoding.UTF8); + return FilterEngine.ParseRuleLines(content); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Do not replace a known-good live snapshot with incomplete rules merely + // because an editor still owns the file. The controller keeps the previous + // immutable snapshot and retries on the next file-system/configuration event. + throw new IOException("Managed routing could not read user-rule.txt while rebuilding the snapshot.", exception); + } + } + } +} diff --git a/shadowsocks-csharp/Controller/Service/OnlineConfigResolver.cs b/Shadowsocks.Core/Controller/Service/OnlineConfigResolver.cs similarity index 91% rename from shadowsocks-csharp/Controller/Service/OnlineConfigResolver.cs rename to Shadowsocks.Core/Controller/Service/OnlineConfigResolver.cs index 31059b73e..e6a3b206c 100644 --- a/shadowsocks-csharp/Controller/Service/OnlineConfigResolver.cs +++ b/Shadowsocks.Core/Controller/Service/OnlineConfigResolver.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -11,14 +10,15 @@ namespace Shadowsocks.Controller.Service { public class OnlineConfigResolver { - public static async Task> GetOnline(string url) + public static async Task> GetOnline(HttpClient httpClient, string url) { - var httpClient = Program.MainController.GetHttpClient(); + ArgumentNullException.ThrowIfNull(httpClient); string server_json = await httpClient.GetStringAsync(url); var servers = server_json.GetServers(); foreach (var server in servers) { server.group = url; + server.importedFromUrl = true; } return servers.ToList(); } diff --git a/Shadowsocks.Core/Controller/Service/OnlinePacCache.cs b/Shadowsocks.Core/Controller/Service/OnlinePacCache.cs new file mode 100644 index 000000000..957c1618f --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/OnlinePacCache.cs @@ -0,0 +1,189 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NLog; +using Shadowsocks.Model; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Persistent cache for a user-supplied online PAC file. + /// Windows never fetches the remote PAC URL directly: PACServer serves this cache + /// from localhost while refreshes are explicitly downloaded through Shadowsocks. + /// + internal static class OnlinePacCache + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly SemaphoreSlim refreshLock = new SemaphoreSlim(1, 1); + + private static readonly string CachePath = AppStoragePaths.OnlinePacCacheFile; + private static readonly string MetadataPath = AppStoragePaths.OnlinePacMetadataFile; + + private const long MaxPacSize = 32L * 1024L * 1024L; + + private sealed class CacheMetadata + { + public string SourceUrl { get; set; } = ""; + public string ETag { get; set; } = ""; + public DateTimeOffset? LastModified { get; set; } + public DateTimeOffset UpdatedUtc { get; set; } + } + + public static bool TryGetContent(string sourceUrl, out string content) + { + content = null; + if (string.IsNullOrWhiteSpace(sourceUrl) || !File.Exists(CachePath)) + return false; + + try + { + CacheMetadata metadata = ReadMetadata(); + if (metadata == null || !string.Equals(metadata.SourceUrl, sourceUrl, StringComparison.Ordinal)) + return false; + + string cached = File.ReadAllText(CachePath, Encoding.UTF8); + if (!IsValidPac(cached)) + return false; + + content = cached; + return true; + } + catch (Exception ex) + { + logger.LogUsefulException(ex); + return false; + } + } + + public static async Task RefreshAsync(Configuration config) + { + ArgumentNullException.ThrowIfNull(config); + if (string.IsNullOrWhiteSpace(config.pacUrl)) + throw new InvalidOperationException("Online PAC URL is empty."); + + await refreshLock.WaitAsync().ConfigureAwait(false); + try + { + CacheMetadata oldMetadata = ReadMetadata(); + bool sameSource = TryGetContent(config.pacUrl, out _); + + using HttpClient client = LocalProxyHttpClient.Create(config); + using var request = new HttpRequestMessage(HttpMethod.Get, config.pacUrl); + + if (sameSource) + { + if (!string.IsNullOrWhiteSpace(oldMetadata.ETag)) + request.Headers.TryAddWithoutValidation("If-None-Match", oldMetadata.ETag); + if (oldMetadata.LastModified.HasValue) + request.Headers.IfModifiedSince = oldMetadata.LastModified; + } + + logger.Info($"Refreshing online PAC through Shadowsocks: {config.pacUrl}"); + using HttpResponseMessage response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotModified && sameSource) + { + logger.Info("Online PAC cache is up to date (HTTP 304)."); + return false; + } + + response.EnsureSuccessStatusCode(); + if (response.Content.Headers.ContentLength is long contentLength && contentLength > MaxPacSize) + throw new InvalidDataException($"Online PAC is too large ({contentLength} bytes)."); + + byte[] bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + if (bytes.LongLength > MaxPacSize) + throw new InvalidDataException($"Online PAC is too large ({bytes.LongLength} bytes)."); + + string content = DecodePac(bytes, response.Content.Headers.ContentType?.CharSet); + if (!IsValidPac(content)) + throw new InvalidDataException("Downloaded online PAC does not contain FindProxyForURL()."); + + string previousContent = null; + if (sameSource) + { + try + { + previousContent = File.ReadAllText(CachePath, Encoding.UTF8); + } + catch (Exception ex) + { + logger.LogUsefulException(ex); + } + } + + WriteAtomic(CachePath, content); + WriteMetadataAtomic(new CacheMetadata + { + SourceUrl = config.pacUrl, + ETag = response.Headers.ETag?.ToString() ?? "", + LastModified = response.Content.Headers.LastModified, + UpdatedUtc = DateTimeOffset.UtcNow, + }); + + logger.Info("Online PAC cache refreshed successfully."); + return !sameSource || !string.Equals(previousContent, content, StringComparison.Ordinal); + } + finally + { + refreshLock.Release(); + } + } + + private static string DecodePac(byte[] bytes, string charset) + { + if (!string.IsNullOrWhiteSpace(charset)) + { + try + { + return Encoding.GetEncoding(charset.Trim('"')).GetString(bytes); + } + catch (Exception ex) when (ex is ArgumentException or NotSupportedException) + { + // Fall back to UTF-8 below. + } + } + + return Encoding.UTF8.GetString(bytes); + } + + private static bool IsValidPac(string content) + => !string.IsNullOrWhiteSpace(content) && + content.Contains("FindProxyForURL", StringComparison.Ordinal); + + private static CacheMetadata ReadMetadata() + { + if (!File.Exists(MetadataPath)) + return null; + + try + { + return JsonConvert.DeserializeObject(File.ReadAllText(MetadataPath, Encoding.UTF8)); + } + catch (Exception ex) + { + logger.LogUsefulException(ex); + return null; + } + } + + private static void WriteMetadataAtomic(CacheMetadata metadata) + => WriteAtomic(MetadataPath, JsonConvert.SerializeObject(metadata, Formatting.Indented)); + + private static void WriteAtomic(string path, string content) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + string tempPath = path + ".new"; + File.WriteAllText(tempPath, content, new UTF8Encoding(false)); + File.Move(tempPath, path, true); + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/PACDaemon.cs b/Shadowsocks.Core/Controller/Service/PACDaemon.cs new file mode 100644 index 000000000..dcb561566 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/PACDaemon.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller +{ + /// + /// Serves the Local PAC transport shim and watches user-rule.txt. + /// All Local PAC routing policy is evaluated by the managed C# FilterEngine; + /// the PAC file itself only funnels WinINet/WinHTTP traffic into the local proxy. + /// + public class PACDaemon : IDisposable + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public static string PacFile => AppStoragePaths.LocalPacFile; + public static string UserRuleFile => AppStoragePaths.UserRuleFile; + + private FileSystemWatcher PACFileWatcher; + private FileSystemWatcher UserRuleFileWatcher; + + public event EventHandler PACFileChanged; + public event EventHandler UserRuleFileChanged; + + public PACDaemon(Configuration config) + { + ArgumentNullException.ThrowIfNull(config); + Directory.CreateDirectory(AppStoragePaths.PacDataDirectory); + TouchUserRuleFile(); + WriteManagedFunnelPacIfNeeded(); + WatchPacFile(); + WatchUserRuleFile(); + } + + public static void UpdateConfiguration(Configuration newConfig) + { + ArgumentNullException.ThrowIfNull(newConfig); + } + + internal static string TouchUserRuleFile() + { + if (!File.Exists(UserRuleFile)) + File.WriteAllText(UserRuleFile, Shadowsocks.Core.EmbeddedResources.UserRule, Encoding.UTF8); + return UserRuleFile; + } + + internal static string GetPACContent() + { + // PAC is transport only. EasyList/ABP/GeoSite decisions are authoritative in C#. + return GetManagedFunnelPac(); + } + + internal static string GetManagedFunnelPac() + => "function FindProxyForURL(url, host) { return __PROXY__; }\n"; + + private static void WriteManagedFunnelPacIfNeeded() + { + string expected = GetManagedFunnelPac(); + string current = File.Exists(PacFile) + ? FileManager.NonExclusiveReadAllText(PacFile, Encoding.UTF8) + : string.Empty; + if (!string.Equals(current, expected, StringComparison.Ordinal)) + { + File.WriteAllText(PacFile, expected, Encoding.UTF8); + } + } + + private void WatchPacFile() + { + PACFileWatcher?.Dispose(); + PACFileWatcher = new FileSystemWatcher(AppStoragePaths.PacDataDirectory) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, + Filter = Path.GetFileName(PacFile), + EnableRaisingEvents = true, + }; + PACFileWatcher.Changed += PACFileWatcher_Changed; + PACFileWatcher.Created += PACFileWatcher_Changed; + PACFileWatcher.Deleted += PACFileWatcher_Changed; + PACFileWatcher.Renamed += PACFileWatcher_Changed; + } + + private void WatchUserRuleFile() + { + UserRuleFileWatcher?.Dispose(); + UserRuleFileWatcher = new FileSystemWatcher(AppStoragePaths.PacDataDirectory) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, + Filter = Path.GetFileName(UserRuleFile), + EnableRaisingEvents = true, + }; + UserRuleFileWatcher.Changed += UserRuleFileWatcher_Changed; + UserRuleFileWatcher.Created += UserRuleFileWatcher_Changed; + UserRuleFileWatcher.Deleted += UserRuleFileWatcher_Changed; + UserRuleFileWatcher.Renamed += UserRuleFileWatcher_Changed; + } + + private async void PACFileWatcher_Changed(object sender, FileSystemEventArgs e) + { + if (PACFileChanged is null) + { + return; + } + + logger.Info($"Detected: PAC file '{e.Name}' was {e.ChangeType.ToString().ToLowerInvariant()}."); + await DispatchWatcherChangeAsync( + (FileSystemWatcher)sender, + () => PACFileChanged?.Invoke(this, EventArgs.Empty)).ConfigureAwait(false); + } + + private async void UserRuleFileWatcher_Changed(object sender, FileSystemEventArgs e) + { + if (UserRuleFileChanged is null) + { + return; + } + + logger.Info($"Detected: User Rule file '{e.Name}' was {e.ChangeType.ToString().ToLowerInvariant()}."); + await DispatchWatcherChangeAsync( + (FileSystemWatcher)sender, + () => UserRuleFileChanged?.Invoke(this, EventArgs.Empty)).ConfigureAwait(false); + } + + private static async Task DispatchWatcherChangeAsync(FileSystemWatcher watcher, Action callback) + { + try + { + watcher.EnableRaisingEvents = false; + await Task.Delay(10).ConfigureAwait(false); + callback(); + } + catch (ObjectDisposedException) + { + // Expected while the controller is shutting down. + } + catch (Exception exception) + { + logger.Warn(exception, "Unable to process PAC file-system watcher notification."); + } + finally + { + try + { + watcher.EnableRaisingEvents = true; + } + catch (ObjectDisposedException) + { + // Expected while the controller is shutting down. + } + } + } + + public void Dispose() + { + PACFileWatcher?.Dispose(); + PACFileWatcher = null; + UserRuleFileWatcher?.Dispose(); + UserRuleFileWatcher = null; + GC.SuppressFinalize(this); + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/PACServer.cs b/Shadowsocks.Core/Controller/Service/PACServer.cs new file mode 100644 index 000000000..0aad14050 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/PACServer.cs @@ -0,0 +1,202 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using NLog; +using Shadowsocks.Controller.Service; +using Shadowsocks.Encryption; +using Shadowsocks.Model; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller +{ + public class PACServer : Listener.Service + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public const string ResourceName = "pac"; + + private string PacSecret + { + get + { + if (string.IsNullOrEmpty(_cachedPacSecret)) + { + var rd = new byte[32]; + RNG.GetBytes(rd); + _cachedPacSecret = HttpServerUtilityUrlToken.Encode(rd); + } + return _cachedPacSecret; + } + } + + private string _cachedPacSecret = ""; + public string PacUrl { get; private set; } = ""; + + private Configuration _config; + private readonly PACDaemon _pacDaemon; + + public PACServer(PACDaemon pacDaemon) + { + _pacDaemon = pacDaemon; + } + + public void UpdatePACURL(Configuration config) + { + _config = config; + string usedSecret = _config.secureLocalPac ? $"&secret={PacSecret}" : ""; + string contentHash = GetHash(GetContentForHash()); + PacUrl = $"http://{config.LocalHost}:{config.localPort}/{ResourceName}?hash={contentHash}{usedSecret}"; + logger.Debug("Set PAC URL:" + PacUrl); + } + + private string GetContentForHash() + { + if (UseOnlinePac()) + { + if (OnlinePacCache.TryGetContent(_config.pacUrl, out string cached)) + return cached; + + string proxy = $"PROXY {_config.LocalHost}:{_config.localPort};"; + return BuildProxyAllPac(proxy); + } + + return PACDaemon.GetPACContent(); + } + + private static string GetHash(string content) + => HttpServerUtilityUrlToken.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(content))); + + public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) + { + if (socket.ProtocolType != ProtocolType.Tcp) + return false; + + try + { + string request = Encoding.UTF8.GetString(firstPacket, 0, length); + string[] lines = request.Split('\r', '\n'); + bool hostMatch = false, pathMatch = false; + bool secretMatch = !_config.secureLocalPac; + + if (lines.Length < 2) + return false; + + string requestLine = lines[0]; + string[] requestItems = requestLine.Split(' '); + if (requestItems.Length == 3 && requestItems[0] == "GET") + { + int index = requestItems[1].IndexOf('?'); + if (index < 0) + index = requestItems[1].Length; + + string resourceString = requestItems[1][1..index]; + if (string.Equals(resourceString, ResourceName, StringComparison.OrdinalIgnoreCase)) + { + pathMatch = true; + if (!secretMatch) + { + string queryString = requestItems[1][index..]; + if (queryString.Contains(PacSecret)) + secretMatch = true; + } + } + } + + for (int i = 1; i < lines.Length; i++) + { + if (string.IsNullOrEmpty(lines[i])) + continue; + + string[] kv = lines[i].Split(':', 2); + if (kv.Length == 2 && kv[0] == "Host" && + kv[1].Trim() == ((IPEndPoint)socket.LocalEndPoint).ToString()) + { + hostMatch = true; + } + } + + if (!hostMatch || !pathMatch) + return false; + + if (!secretMatch) + socket.Close(); + else + SendResponse(socket); + + return true; + } + catch (ArgumentException) + { + return false; + } + } + + private void SendResponse(Socket socket) + { + try + { + IPEndPoint localEndPoint = (IPEndPoint)socket.LocalEndPoint; + string proxy = GetPACAddress(localEndPoint); + string pacContent; + + if (UseOnlinePac()) + { + // Serve the remote PAC byte-for-byte logically from the local cache. + // If this is the first run and the cache is not ready yet, proxy-all is + // a safe bootstrap that avoids a direct-network leak. + pacContent = OnlinePacCache.TryGetContent(_config.pacUrl, out string cached) + ? cached + : BuildProxyAllPac(proxy); + } + else + { + pacContent = $"var __PROXY__ = '{proxy}';\n" + PACDaemon.GetPACContent(); + } + + byte[] body = Encoding.UTF8.GetBytes(pacContent); + string responseHead = + $"HTTP/1.1 200 OK\r\n" + + $"Server: Shadowsocks-Reborn/{ApplicationInfo.Version}\r\n" + + "Content-Type: application/x-ns-proxy-autoconfig; charset=utf-8\r\n" + + $"Content-Length: {body.Length}\r\n" + + "Cache-Control: no-cache\r\n" + + "Connection: Close\r\n\r\n"; + byte[] head = Encoding.UTF8.GetBytes(responseHead); + byte[] response = new byte[head.Length + body.Length]; + Buffer.BlockCopy(head, 0, response, 0, head.Length); + Buffer.BlockCopy(body, 0, response, head.Length, body.Length); + socket.BeginSend(response, 0, response.Length, 0, SendCallback, socket); + } + catch (Exception e) + { + logger.LogUsefulException(e); + socket.Close(); + } + } + + private static void SendCallback(IAsyncResult ar) + { + Socket conn = (Socket)ar.AsyncState; + try + { + conn.Shutdown(SocketShutdown.Send); + } + catch + { + } + } + + private bool UseOnlinePac() + => _config != null && _config.useOnlinePac && !string.IsNullOrWhiteSpace(_config.pacUrl); + + private static string BuildProxyAllPac(string proxy) + => $"function FindProxyForURL(url, host) {{ return '{proxy}'; }}\n"; + + private string GetPACAddress(IPEndPoint localEndPoint) + => localEndPoint.AddressFamily == AddressFamily.InterNetworkV6 + ? $"PROXY [{localEndPoint.Address}]:{_config.localPort};" + : $"PROXY {localEndPoint.Address}:{_config.localPort};"; + } +} diff --git a/Shadowsocks.Core/Controller/Service/Socks5Connector.cs b/Shadowsocks.Core/Controller/Service/Socks5Connector.cs new file mode 100644 index 000000000..637ce9a16 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/Socks5Connector.cs @@ -0,0 +1,140 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Shadowsocks.Controller.Service +{ + internal static class Socks5Connector + { + public static async Task ConnectAsync( + string proxyHost, + int proxyPort, + string destinationHost, + int destinationPort, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(proxyHost); + ArgumentException.ThrowIfNullOrWhiteSpace(destinationHost); + + string normalizedProxyHost = proxyHost.Trim().TrimStart('[').TrimEnd(']'); + Socket socket = new( + normalizedProxyHost.Contains(':') ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork, + SocketType.Stream, + ProtocolType.Tcp) + { + NoDelay = true, + }; + + try + { + await socket.ConnectAsync(normalizedProxyHost, proxyPort, cancellationToken).ConfigureAwait(false); + using NetworkStream stream = new(socket, ownsSocket: false); + + await stream.WriteAsync(new byte[] { 0x05, 0x01, 0x00 }, cancellationToken).ConfigureAwait(false); + byte[] greeting = new byte[2]; + await ReadExactlyAsync(stream, greeting, cancellationToken).ConfigureAwait(false); + if (greeting[0] != 0x05 || greeting[1] != 0x00) + { + throw new IOException("Local SOCKS5 listener rejected no-authentication negotiation."); + } + + byte[] request = BuildConnectRequest(destinationHost, destinationPort); + await stream.WriteAsync(request, cancellationToken).ConfigureAwait(false); + + byte[] responseHeader = new byte[4]; + await ReadExactlyAsync(stream, responseHeader, cancellationToken).ConfigureAwait(false); + if (responseHeader[0] != 0x05 || responseHeader[1] != 0x00) + { + throw new IOException($"Local SOCKS5 CONNECT failed with reply 0x{responseHeader[1]:X2}."); + } + + int remainingAddressBytes = responseHeader[3] switch + { + 0x01 => 4, + 0x04 => 16, + 0x03 => await ReadDomainLengthAsync(stream, cancellationToken).ConfigureAwait(false), + _ => throw new IOException($"Local SOCKS5 listener returned unsupported address type 0x{responseHeader[3]:X2}."), + }; + + if (remainingAddressBytes > 0) + { + byte[] discardAddress = new byte[remainingAddressBytes]; + await ReadExactlyAsync(stream, discardAddress, cancellationToken).ConfigureAwait(false); + } + + byte[] discardPort = new byte[2]; + await ReadExactlyAsync(stream, discardPort, cancellationToken).ConfigureAwait(false); + return socket; + } + catch + { + socket.Dispose(); + throw; + } + } + + private static byte[] BuildConnectRequest(string host, int port) + { + if (port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort) + { + throw new ArgumentOutOfRangeException(nameof(port)); + } + + using MemoryStream buffer = new(); + buffer.WriteByte(0x05); + buffer.WriteByte(0x01); + buffer.WriteByte(0x00); + + if (IPAddress.TryParse(host.Trim('[', ']'), out IPAddress address)) + { + byte[] addressBytes = address.GetAddressBytes(); + buffer.WriteByte(address.AddressFamily == AddressFamily.InterNetwork ? (byte)0x01 : (byte)0x04); + buffer.Write(addressBytes, 0, addressBytes.Length); + } + else + { + byte[] hostBytes = Encoding.UTF8.GetBytes(host); + if (hostBytes.Length is 0 or > 255) + { + throw new ArgumentException("SOCKS5 destination hostname must be between 1 and 255 bytes.", nameof(host)); + } + + buffer.WriteByte(0x03); + buffer.WriteByte((byte)hostBytes.Length); + buffer.Write(hostBytes, 0, hostBytes.Length); + } + + Span portBytes = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(portBytes, checked((ushort)port)); + buffer.Write(portBytes); + return buffer.ToArray(); + } + + private static async Task ReadDomainLengthAsync(Stream stream, CancellationToken cancellationToken) + { + byte[] length = new byte[1]; + await ReadExactlyAsync(stream, length, cancellationToken).ConfigureAwait(false); + return length[0]; + } + + private static async Task ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) + { + int offset = 0; + while (offset < buffer.Length) + { + int read = await stream.ReadAsync(buffer[offset..], cancellationToken).ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException("Unexpected EOF while negotiating SOCKS5 connection."); + } + + offset += read; + } + } + } +} diff --git a/Shadowsocks.Core/Controller/Service/TransportBufferSizes.cs b/Shadowsocks.Core/Controller/Service/TransportBufferSizes.cs new file mode 100644 index 000000000..fafb5b8a7 --- /dev/null +++ b/Shadowsocks.Core/Controller/Service/TransportBufferSizes.cs @@ -0,0 +1,13 @@ +using Shadowsocks.Encryption.AEAD; + +namespace Shadowsocks.Controller.Service +{ + /// Shared transport buffer limits used by the protocol and Windows relay. + public static class TransportBufferSizes + { + public const int ReceiveSize = 2048; + public const int ChunkOverheadSize = 16 * 2 + AEADEncryptor.ChunkLengthBytes; + public const uint MaxChunkSize = AEADEncryptor.ChunkLengthMask + AEADEncryptor.ChunkLengthBytes + 16 * 2; + public const int BufferSize = ReceiveSize + (int)MaxChunkSize + 32; + } +} diff --git a/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs b/Shadowsocks.Core/Controller/Strategy/BalancingStrategy.cs similarity index 58% rename from shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs rename to Shadowsocks.Core/Controller/Strategy/BalancingStrategy.cs index 82e30eb8b..4df32540a 100644 --- a/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs +++ b/Shadowsocks.Core/Controller/Strategy/BalancingStrategy.cs @@ -1,71 +1,75 @@ -using Shadowsocks.Controller; -using Shadowsocks.Model; -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - -namespace Shadowsocks.Controller.Strategy -{ - class BalancingStrategy : IStrategy - { - ShadowsocksController _controller; - Random _random; - - public BalancingStrategy(ShadowsocksController controller) - { - _controller = controller; - _random = new Random(); - } - - public string Name - { - get { return I18N.GetString("Load Balance"); } - } - - public string ID - { - get { return "com.shadowsocks.strategy.balancing"; } - } - - public void ReloadServers() - { - // do nothing - } - - public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint) - { - var configs = _controller.GetCurrentConfiguration().configs; - int index; - if (type == IStrategyCallerType.TCP) - { - index = _random.Next(); - } - else - { - index = localIPEndPoint.GetHashCode(); - } - return configs[index % configs.Count]; - } - - public void UpdateLatency(Model.Server server, TimeSpan latency) - { - // do nothing - } - - public void UpdateLastRead(Model.Server server) - { - // do nothing - } - - public void UpdateLastWrite(Model.Server server) - { - // do nothing - } - - public void SetFailure(Model.Server server) - { - // do nothing - } - } -} +using System; +using System.Net; +using System.Linq; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Strategy +{ + class BalancingStrategy : IStrategy + { + private readonly Func _configurationProvider; + private readonly Random _random; + + public BalancingStrategy(Func configurationProvider) + { + ArgumentNullException.ThrowIfNull(configurationProvider); + _configurationProvider = configurationProvider; + _random = new Random(); + } + + public string Name + { + get { return I18N.GetString("Load Balance"); } + } + + public string ID + { + get { return "com.shadowsocks.strategy.balancing"; } + } + + public void ReloadServers() + { + // do nothing + } + + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint) + { + Server[] configs = (_configurationProvider().configs ?? []) + .Where(server => server?.IsConfigured == true) + .ToArray(); + if (configs.Length == 0) + { + return null; + } + + if (type == IStrategyCallerType.TCP) + { + return configs[_random.Next(configs.Length)]; + } + + int index = localIPEndPoint.GetHashCode(); + int selectedIndex = (int)((uint)index % (uint)configs.Length); + return configs[selectedIndex]; + } + + public void UpdateLatency(Model.Server server, TimeSpan latency) + { + // do nothing + } + + public void UpdateLastRead(Model.Server server) + { + // do nothing + } + + public void UpdateLastWrite(Model.Server server) + { + // do nothing + } + + public void SetFailure(Model.Server server) + { + // do nothing + } + } +} diff --git a/Shadowsocks.Core/Controller/Strategy/HighAvailabilityStrategy.cs b/Shadowsocks.Core/Controller/Strategy/HighAvailabilityStrategy.cs new file mode 100644 index 000000000..9648b234c --- /dev/null +++ b/Shadowsocks.Core/Controller/Strategy/HighAvailabilityStrategy.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Net; +using NLog; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Strategy +{ + class HighAvailabilityStrategy : IStrategy + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + private ServerStatus _currentServer; + private Dictionary _serverStatus; + private readonly Func _configurationProvider; + + public class ServerStatus + { + // time interval between SYN and SYN+ACK + public TimeSpan Latency { get; set; } + public DateTime LastTimeDetectLatency { get; set; } + + // last time anything received + public DateTime LastRead { get; set; } + + // last time anything sent + public DateTime LastWrite { get; set; } + + // connection refused or closed before anything received + public DateTime LastFailure { get; set; } + + public Server Server { get; set; } + + public double Score { get; set; } + } + + public HighAvailabilityStrategy(Func configurationProvider) + { + ArgumentNullException.ThrowIfNull(configurationProvider); + _configurationProvider = configurationProvider; + _serverStatus = new Dictionary(ReferenceEqualityComparer.Instance); + } + + public string Name + { + get { return I18N.GetString("High Availability"); } + } + + public string ID + { + get { return "com.shadowsocks.strategy.ha"; } + } + + public void ReloadServers() + { + Dictionary previous = _serverStatus; + Dictionary current = new(ReferenceEqualityComparer.Instance); + DateTime now = DateTime.Now; + + foreach (Server server in _configurationProvider().configs ?? []) + { + if (server?.IsConfigured != true) + { + continue; + } + + if (!previous.TryGetValue(server, out ServerStatus status)) + { + status = new ServerStatus + { + Server = server, + LastFailure = DateTime.MinValue, + LastRead = now, + LastWrite = now, + Latency = TimeSpan.FromMilliseconds(10), + LastTimeDetectLatency = now, + }; + } + else + { + status.Server = server; + } + + current[server] = status; + } + + _serverStatus = current; + if (_currentServer is not null && !current.ContainsValue(_currentServer)) + { + _currentServer = null; + } + + ChooseNewServer(); + } + + public Server GetAServer(IStrategyCallerType type, System.Net.IPEndPoint localIPEndPoint, EndPoint destEndPoint) + { + if (type == IStrategyCallerType.TCP) + { + ChooseNewServer(); + } + if (_currentServer == null) + { + return null; + } + return _currentServer.Server; + } + + /** + * once failed, try after 5 min + * and (last write - last read) < 5s + * and (now - last read) < 5s // means not stuck + * and latency < 200ms, try after 30s + */ + public void ChooseNewServer() + { + DateTime now = DateTime.Now; + ServerStatus best = null; + + foreach (ServerStatus status in _serverStatus.Values) + { + status.Score = + 100 * 1000 * Math.Min(5 * 60, (now - status.LastFailure).TotalSeconds) + - 2 * 5 * (Math.Min(2000, status.Latency.TotalMilliseconds) / + (1 + (now - status.LastTimeDetectLatency).TotalSeconds / 300)) + + 0.5 * 200 * Math.Min(5, (status.LastWrite - status.LastRead).TotalSeconds); + + logger.Debug($"server: {status.Server} latency:{status.Latency} score: {status.Score}"); + if (best is null || status.Score >= best.Score) + { + best = status; + } + } + + if (best is not null && (_currentServer is null || best.Score - _currentServer.Score > 200)) + { + _currentServer = best; + logger.Info($"HA switching to server: {_currentServer.Server}"); + } + } + + public void UpdateLatency(Model.Server server, TimeSpan latency) + { + logger.Debug($"latency: {server.ToString()} {latency}"); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.Latency = latency; + status.LastTimeDetectLatency = DateTime.Now; + } + } + + public void UpdateLastRead(Model.Server server) + { + logger.Debug($"last read: {server.ToString()}"); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.LastRead = DateTime.Now; + } + } + + public void UpdateLastWrite(Model.Server server) + { + logger.Debug($"last write: {server.ToString()}"); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.LastWrite = DateTime.Now; + } + } + + public void SetFailure(Model.Server server) + { + logger.Debug($"failure: {server.ToString()}"); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.LastFailure = DateTime.Now; + } + } + } +} diff --git a/shadowsocks-csharp/Controller/Strategy/IStrategy.cs b/Shadowsocks.Core/Controller/Strategy/IStrategy.cs similarity index 88% rename from shadowsocks-csharp/Controller/Strategy/IStrategy.cs rename to Shadowsocks.Core/Controller/Strategy/IStrategy.cs index 48bd3aca5..7e6aafad2 100644 --- a/shadowsocks-csharp/Controller/Strategy/IStrategy.cs +++ b/Shadowsocks.Core/Controller/Strategy/IStrategy.cs @@ -1,56 +1,54 @@ -using Shadowsocks.Model; -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - -namespace Shadowsocks.Controller.Strategy -{ - public enum IStrategyCallerType - { - TCP, - UDP - } - - /* - * IStrategy - * - * Subclasses must be thread-safe - */ - public interface IStrategy - { - string Name { get; } - - string ID { get; } - - /* - * Called when servers need to be reloaded, i.e. new configuration saved - */ - void ReloadServers(); - - /* - * Get a new server to use in TCPRelay or UDPRelay - */ - Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint); - - /* - * TCPRelay will call this when latency of a server detected - */ - void UpdateLatency(Server server, TimeSpan latency); - - /* - * TCPRelay will call this when reading from a server - */ - void UpdateLastRead(Server server); - - /* - * TCPRelay will call this when writing to a server - */ - void UpdateLastWrite(Server server); - - /* - * TCPRelay will call this when fatal failure detected - */ - void SetFailure(Server server); - } -} +using System; +using System.Net; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Strategy +{ + public enum IStrategyCallerType + { + TCP, + UDP + } + + /* + * IStrategy + * + * Subclasses must be thread-safe + */ + public interface IStrategy + { + string Name { get; } + + string ID { get; } + + /* + * Called when servers need to be reloaded, i.e. new configuration saved + */ + void ReloadServers(); + + /* + * Get a new server to use in TCPRelay or UDPRelay + */ + Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint); + + /* + * TCPRelay will call this when latency of a server detected + */ + void UpdateLatency(Server server, TimeSpan latency); + + /* + * TCPRelay will call this when reading from a server + */ + void UpdateLastRead(Server server); + + /* + * TCPRelay will call this when writing to a server + */ + void UpdateLastWrite(Server server); + + /* + * TCPRelay will call this when fatal failure detected + */ + void SetFailure(Server server); + } +} diff --git a/Shadowsocks.Core/Controller/Strategy/StrategyManager.cs b/Shadowsocks.Core/Controller/Strategy/StrategyManager.cs new file mode 100644 index 000000000..ac0e4d039 --- /dev/null +++ b/Shadowsocks.Core/Controller/Strategy/StrategyManager.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Strategy +{ + class StrategyManager + { + private readonly List _strategies; + public StrategyManager(Func configurationProvider) + { + ArgumentNullException.ThrowIfNull(configurationProvider); + _strategies = + [ + new BalancingStrategy(configurationProvider), + new HighAvailabilityStrategy(configurationProvider), + ]; + } + public IList GetStrategies() + { + return _strategies; + } + } +} diff --git a/Shadowsocks.Core/Controller/Traffic/ApplicationPatternMatcher.cs b/Shadowsocks.Core/Controller/Traffic/ApplicationPatternMatcher.cs new file mode 100644 index 000000000..cdce43db5 --- /dev/null +++ b/Shadowsocks.Core/Controller/Traffic/ApplicationPatternMatcher.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; + +namespace Shadowsocks.Controller.Traffic +{ + internal static class ApplicationPatternMatcher + { + public static bool Matches(string pattern, string processPath, string processName) + { + if (!TryCompile(pattern, out CompiledApplicationPattern compiled)) + { + return false; + } + + ApplicationMatchTarget target = CreateTarget(processPath, processName); + return Matches(compiled, target); + } + + internal static bool TryCompile(string pattern, out CompiledApplicationPattern compiled) + { + if (string.IsNullOrWhiteSpace(pattern)) + { + compiled = default; + return false; + } + + string normalized = pattern.Trim(); + compiled = new CompiledApplicationPattern( + normalized, + Path.GetFileName(normalized), + normalized.Contains('*') || normalized.Contains('?')); + return true; + } + + internal static ApplicationMatchTarget CreateTarget(string processPath, string processName) + { + string path = processPath ?? string.Empty; + string fileName = path.Length == 0 ? string.Empty : Path.GetFileName(path); + string name = processName ?? fileName; + string nameWithExe = name.Length == 0 || name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ? name + : string.Concat(name, ".exe"); + return new ApplicationMatchTarget(path, fileName, name, nameWithExe); + } + + internal static bool Matches(CompiledApplicationPattern compiled, ApplicationMatchTarget target) + { + if (!compiled.HasWildcards) + { + return string.Equals(compiled.Pattern, target.Path, StringComparison.OrdinalIgnoreCase) + || string.Equals(compiled.Pattern, target.FileName, StringComparison.OrdinalIgnoreCase) + || string.Equals(compiled.Pattern, target.Name, StringComparison.OrdinalIgnoreCase) + || string.Equals(compiled.Pattern, target.NameWithExe, StringComparison.OrdinalIgnoreCase) + || string.Equals(compiled.FileName, target.Name, StringComparison.OrdinalIgnoreCase) + || string.Equals(compiled.FileName, target.NameWithExe, StringComparison.OrdinalIgnoreCase); + } + + return WildcardMatch(target.Path, compiled.Pattern) + || WildcardMatch(target.FileName, compiled.Pattern) + || WildcardMatch(target.Name, compiled.Pattern) + || WildcardMatch(target.NameWithExe, compiled.Pattern); + } + + public static bool WildcardMatch(string value, string pattern) + { + value ??= string.Empty; + pattern ??= string.Empty; + + int valueIndex = 0; + int patternIndex = 0; + int starIndex = -1; + int checkpoint = 0; + + while (valueIndex < value.Length) + { + if (patternIndex < pattern.Length + && (pattern[patternIndex] == '?' + || char.ToUpperInvariant(pattern[patternIndex]) == char.ToUpperInvariant(value[valueIndex]))) + { + valueIndex++; + patternIndex++; + } + else if (patternIndex < pattern.Length && pattern[patternIndex] == '*') + { + starIndex = patternIndex++; + checkpoint = valueIndex; + } + else if (starIndex >= 0) + { + patternIndex = starIndex + 1; + valueIndex = ++checkpoint; + } + else + { + return false; + } + } + + while (patternIndex < pattern.Length && pattern[patternIndex] == '*') + { + patternIndex++; + } + + return patternIndex == pattern.Length; + } + + internal readonly record struct CompiledApplicationPattern( + string Pattern, + string FileName, + bool HasWildcards); + + internal readonly record struct ApplicationMatchTarget( + string Path, + string FileName, + string Name, + string NameWithExe); + } +} diff --git a/Shadowsocks.Core/Controller/Traffic/ApplicationRouteRule.cs b/Shadowsocks.Core/Controller/Traffic/ApplicationRouteRule.cs new file mode 100644 index 000000000..355c759b1 --- /dev/null +++ b/Shadowsocks.Core/Controller/Traffic/ApplicationRouteRule.cs @@ -0,0 +1,18 @@ +using System; +using Newtonsoft.Json; + +namespace Shadowsocks.Controller.Traffic +{ + [Serializable] + public sealed class ApplicationRouteRule + { + [JsonProperty("enabled")] + public bool Enabled { get; set; } = true; + + [JsonProperty("application")] + public string Application { get; set; } = string.Empty; + + [JsonProperty("action")] + public TrafficRouteAction Action { get; set; } = TrafficRouteAction.Proxy; + } +} diff --git a/Shadowsocks.Core/Controller/Traffic/DnsPolicyConfig.cs b/Shadowsocks.Core/Controller/Traffic/DnsPolicyConfig.cs new file mode 100644 index 000000000..dc33a9d8d --- /dev/null +++ b/Shadowsocks.Core/Controller/Traffic/DnsPolicyConfig.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace Shadowsocks.Controller.Traffic +{ + /// + /// DNS routing contract. DNSCrypt runtime configuration is persisted here; + /// Admin Mode consumes the live DNSCrypt port/PID and managed component root through the NetworkService IPC. + /// + [Serializable] + public sealed class DnsPolicyConfig + { + public DnsPolicyMode mode { get; set; } = DnsPolicyMode.System; + public string directDnsServer { get; set; } = string.Empty; + public string directDnsFallbackServer { get; set; } = string.Empty; + public bool directDnsRouteThroughShadowsocks { get; set; } + public string customDohUrl { get; set; } = string.Empty; + public bool customDohRouteThroughShadowsocks { get; set; } + public DnsCryptConfig dnsCrypt { get; set; } = new(); + } + + [Serializable] + public sealed class DnsCryptConfig + { + public bool autoUpdate { get; set; } = true; + public bool requireDnssec { get; set; } = true; + public bool requireNoLog { get; set; } = true; + public bool requireNoFilter { get; set; } = true; + public bool ipv4Servers { get; set; } = true; + public bool ipv6Servers { get; set; } + public bool routeThroughShadowsocks { get; set; } = true; + // Explicit resolver selection mode. Automatic mode keeps serverNames runtime-managed. + public bool automaticResolvers { get; set; } = true; + public bool failClosed { get; set; } = true; + public List serverNames { get; set; } = new(); + } +} diff --git a/Shadowsocks.Core/Controller/Traffic/TrafficModels.cs b/Shadowsocks.Core/Controller/Traffic/TrafficModels.cs new file mode 100644 index 000000000..be20f9078 --- /dev/null +++ b/Shadowsocks.Core/Controller/Traffic/TrafficModels.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Net; + +namespace Shadowsocks.Controller.Traffic +{ + public enum TrafficRouteAction + { + Default = 0, + Proxy = 1, + Direct = 2, + Block = 3, + } + + public enum TrafficCaptureMode + { + User = 0, + Admin = 1, + } + + public enum TrafficRuntimeMode + { + User = 0, + Admin = 1, + Game = 2, + } + + public enum DnsPolicyMode + { + System = 0, + Direct = 1, + Proxy = 2, + CustomDoh = 3, + DnsCrypt = 4, + } + + public sealed record ManagedRoutingStatus( + string EffectiveMode, + string SnapshotMode, + long Generation, + bool Enabled, + string Source, + System.DateTime CreatedUtc, + int DefaultRuleCount, + int UserRuleCount, + int InvalidRuleCount, + long DirectDecisionCount, + long ProxyDecisionCount, + bool AdminManagedRoutingActive, + int AdminManagedRoutingRuleCount); + + public sealed record TrafficCaptureStatus( + TrafficCaptureMode ConfiguredMode, + TrafficRuntimeMode RuntimeMode, + bool NetworkServiceRunning, + bool WinDivertActive, + bool GameModeActive, + bool TcpCaptureActive, + bool UdpCaptureActive, + bool DnsInterceptionActive, + bool DnsFailClosedActive, + int TcpRedirectPort, + int UdpRedirectPort, + IReadOnlyList RunningGameApplications); + + /// + /// Ephemeral DNSCrypt runtime data passed to the elevated capture service. + /// This state is intentionally not persisted in settings because the listener + /// port and process identifier change whenever DNSCrypt restarts. + /// + public readonly record struct DnsCaptureRuntimeState(int Port, int ProcessId) + { + public bool IsReady => Port is >= 1 and <= 65535 && ProcessId > 0; + public static DnsCaptureRuntimeState Unavailable => default; + } + + public sealed class TrafficContext + { + public int ProcessId { get; init; } + public string ProcessPath { get; init; } + public string ProcessName { get; init; } + public string Protocol { get; init; } + public string DestinationHost { get; init; } + public string DestinationUrl { get; init; } + public IPAddress DestinationAddress { get; init; } + public int DestinationPort { get; init; } + public bool IsInternal { get; init; } + } + + public readonly record struct RouteDecision(TrafficRouteAction Action, string Reason) + { + public static RouteDecision Proxy(string reason) => new(TrafficRouteAction.Proxy, reason); + public static RouteDecision Direct(string reason) => new(TrafficRouteAction.Direct, reason); + public static RouteDecision Block(string reason) => new(TrafficRouteAction.Block, reason); + } +} diff --git a/Shadowsocks.Core/Controller/Traffic/TrafficPolicyEngine.cs b/Shadowsocks.Core/Controller/Traffic/TrafficPolicyEngine.cs new file mode 100644 index 000000000..d7ea2b9eb --- /dev/null +++ b/Shadowsocks.Core/Controller/Traffic/TrafficPolicyEngine.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Shadowsocks.Model; +using Shadowsocks.Routing; + +namespace Shadowsocks.Controller.Traffic +{ + /// + /// Shared routing policy used by both user-mode proxying and the elevated + /// transparent-capture backend. Capture and routing are deliberately separate. + /// + public sealed class TrafficPolicyEngine + { + private ApplicationRuleSnapshot[] _applicationRules = []; + private ManagedRoutingSnapshot _managedRouting = ManagedRoutingSnapshot.Disabled("not initialized"); + + public TrafficPolicyEngine(Configuration configuration) + { + UpdateConfiguration(configuration); + } + + public void UpdateConfiguration(Configuration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + List snapshots = []; + if (configuration.applicationRules is not null) + { + foreach (ApplicationRouteRule rule in configuration.applicationRules) + { + if (rule is null || !rule.Enabled || string.IsNullOrWhiteSpace(rule.Application)) + { + continue; + } + + if (ApplicationPatternMatcher.TryCompile( + rule.Application, + out ApplicationPatternMatcher.CompiledApplicationPattern pattern)) + { + snapshots.Add(new ApplicationRuleSnapshot(pattern, rule.Action)); + } + } + } + + Volatile.Write(ref _applicationRules, snapshots.ToArray()); + } + + public ManagedRoutingSnapshot CurrentManagedRoutingSnapshot => Volatile.Read(ref _managedRouting); + public long ManagedDirectDecisionCount => CurrentManagedRoutingSnapshot.DirectDecisionCount; + public long ManagedProxyDecisionCount => CurrentManagedRoutingSnapshot.ProxyDecisionCount; + + public void UpdateManagedRouting(ManagedRoutingSnapshot snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + Volatile.Write(ref _managedRouting, snapshot); + } + + public RouteDecision EvaluateUserMode(TrafficContext context) + { + ArgumentNullException.ThrowIfNull(context); + + // Internal maintenance requests (Online PAC / GeoSite / update checks that + // explicitly enter the local proxy) must never be turned DIRECT by an app rule. + if (context.IsInternal) + { + return RouteDecision.Proxy("internal Shadowsocks request"); + } + + TrafficRouteAction applicationAction = MatchApplication(context.ProcessPath, context.ProcessName); + if (applicationAction != TrafficRouteAction.Default) + { + return applicationAction switch + { + TrafficRouteAction.Direct => RouteDecision.Direct("application rule"), + TrafficRouteAction.Block => RouteDecision.Block("application rule"), + _ => RouteDecision.Proxy("application rule"), + }; + } + + RouteDecision? managed = EvaluateManagedRouting(context); + return managed ?? RouteDecision.Proxy("system proxy/PAC selected this request"); + } + + public RouteDecision EvaluateAdminMode(TrafficContext context, TrafficRouteAction fallback) + { + ArgumentNullException.ThrowIfNull(context); + + if (context.IsInternal) + { + return RouteDecision.Direct("Shadowsocks transport exclusion"); + } + + TrafficRouteAction applicationAction = MatchApplication(context.ProcessPath, context.ProcessName); + if (applicationAction != TrafficRouteAction.Default) + { + return applicationAction switch + { + TrafficRouteAction.Block => RouteDecision.Block("application/admin rule"), + TrafficRouteAction.Direct => RouteDecision.Direct("application/admin rule"), + _ => RouteDecision.Proxy("application/admin rule"), + }; + } + + // Admin SYN classification may not have a hostname yet. Ordinary TCP is deferred + // to the transparent relay, where HTTP Host or TLS ClientHello SNI supplies + // DestinationUrl and this exact managed decision path can be reused. UDP and + // hostname-less traffic still use the deterministic fallback below. + RouteDecision? managed = EvaluateManagedRouting(context); + if (managed is not null) + { + return managed.Value; + } + + return fallback switch + { + TrafficRouteAction.Block => RouteDecision.Block("admin fallback"), + TrafficRouteAction.Direct => RouteDecision.Direct("admin fallback"), + _ => RouteDecision.Proxy("admin fallback"), + }; + } + + private RouteDecision? EvaluateManagedRouting(TrafficContext context) + { + ManagedRoutingSnapshot snapshot = CurrentManagedRoutingSnapshot; + if (!snapshot.IsEnabled || string.IsNullOrWhiteSpace(context.DestinationUrl)) + { + return null; + } + + FilterRoutingDecision filter = snapshot.Engine.Evaluate(context.DestinationUrl, context.DestinationHost); + string matched = string.IsNullOrWhiteSpace(filter.MatchedRule) + ? filter.Source.ToString() + : $"{filter.Source}: {filter.MatchedRule}"; + string reason = $"managed routing generation {snapshot.Generation} ({matched})"; + snapshot.RecordDecision(filter.Action); + return filter.Action == FilterRoutingAction.Direct + ? RouteDecision.Direct(reason) + : RouteDecision.Proxy(reason); + } + + private TrafficRouteAction MatchApplication(string processPath, string processName) + { + ApplicationRuleSnapshot[] rules = Volatile.Read(ref _applicationRules); + ApplicationPatternMatcher.ApplicationMatchTarget target = + ApplicationPatternMatcher.CreateTarget(processPath, processName); + foreach (ApplicationRuleSnapshot rule in rules) + { + if (ApplicationPatternMatcher.Matches(rule.Pattern, target)) + { + return rule.Action; + } + } + + return TrafficRouteAction.Default; + } + + private readonly record struct ApplicationRuleSnapshot( + ApplicationPatternMatcher.CompiledApplicationPattern Pattern, + TrafficRouteAction Action); + } +} diff --git a/Shadowsocks.Core/Data/i18n.csv b/Shadowsocks.Core/Data/i18n.csv new file mode 100644 index 000000000..64be71e0f --- /dev/null +++ b/Shadowsocks.Core/Data/i18n.csv @@ -0,0 +1,918 @@ +en,ru-RU,zh-CN,zh-TW,ja,ko,fr +#Restart program to apply translation,,,,,, +#This is comment line,,,,,, +#Always keep language name at head of file,,,,,, +#Language name is output in log,,,,,, +"#You can find it by search ""Current language is:""",,,,,, +#Please use UTF-8 with BOM encoding so we can edit it in Excel,,,,,, +shadowsocks-reborn,shadowsocks-reborn,shadowsocks-reborn,shadowsocks-reborn,shadowsocks-reborn,shadowsocks-reborn,shadowsocks-reborn +#Menu,,,,,, +System Proxy,Системный прокси-сервер,系统代理,系統代理,システムプロキシ,시스템 프록시,Proxy système +Disable,Отключен,禁用,禁用,無効,비활성화,Désactiver +PAC,Сценарий настройки (PAC),PAC 模式,PAC 模式,PACモード,프록시 자동 구성 (PAC),PAC +Global,Для всей системы,全局模式,全局模式,グローバルプロキシ,전역,Global +Servers,Серверы,服务器,伺服器,サーバー,서버,Serveurs +More than 20 servers (total: {0}),Более 20 серверов (всего: {0}),超过 20 个服务器(共 {0} 个),超過 20 個伺服器(共 {0} 個),20 台を超えるサーバー(合計: {0}),서버 20개 초과 (총 {0}개),Plus de 20 serveurs (total : {0}) +... more than {0} (total {1}),... показано {0} из {1},… 另有超过 {0} 个(共 {1} 个),… 另有超過 {0} 個(共 {1} 個),… さらに {0} 件以上(合計 {1} 件),… {0}개 이상 추가(총 {1}개),… plus de {0} autres (total {1}) +Online Config,Онлайн-конфигурации,在线配置,線上配置,オンライン設定,온라인 구성,Configuration en ligne +Start on Boot,Автозагрузка,开机启动,開機啟動,システム起動時に実行,시스템 시작 시에 시작하기,Démarrage automatique +Associate ss:// Links,Ассоциированный ss:// Ссылки,关联 ss:// 链接,關聯 ss:// 鏈接,ss:// リンクの関連付け,ss:// 링크 연결,Associer les liens ss:// +Forward Proxy,Прямой прокси,正向代理设置,正向 Proxy 設定,フォワードプロキシの設定,포워드 프록시,Forward-proxy +Local PAC,Локальный PAC,使用本地 PAC,使用本機 PAC,ローカル PAC,로컬 프록시 자동 구성,PAC local +Online PAC,Удаленный PAC,使用在线 PAC,使用線上 PAC,オンライン PAC,온라인 프록시 자동 구성,PAC en ligne +Update Local PAC from Geosite,Обновить локальный PAC из Geosite,从 Geosite 更新本地 PAC,從 Geosite 更新本機 PAC,Geosite からローカル PAC を更新,Geosite에서 로컬 프록시 자동 구성 파일 업데이트,Mettre à jour le PAC local à partir de Geosite +GeoSite Sources,Источники GeoSite,GeoSite 源,GeoSite 來源,GeoSite ソース,GeoSite 소스,Sources GeoSite +Copy Local PAC URL,Копировать URL локального PAC,复制本地 PAC 网址,複製本機 PAC 網址,ローカル PAC URL をコピー,로컬 프록시 자동 구성 파일 URL 복사,Copier l'URL du PAC local +Share Server Config,Поделиться конфигурацией сервера,分享服务器配置,分享伺服器設定檔,サーバーの設定を共有,서버 설정 공유,Partager la configuration du serveur +Scan QRCode from Screen,Сканировать QRCode с экрана,扫描屏幕上的二维码,掃描螢幕上的 QR 碼,画面から QR コードをスキャン,화면에서 QR코드 스캔,Scanner le QRCode à partir de l'écran +Import URL from Clipboard,Импорт адреса из буфера обмена,从剪贴板导入URL,從剪貼簿匯入 URL,クリップボードから URL をインポート,클립보드에서 URL 가져오기,Importer l'URL du presse-papiers +Availability Statistics,Статистика доступности,统计可用性,統計可用性,可用性の統計,가용성 통계,Statistiques de disponibilité +Verbose Logging,Подробный журнал,详细记录日志,詳細資訊記錄,詳細なログを記録,자세한 로깅 사용,Journalisation détaillée +Updates,Обновления,更新,更新,アップデート,업데이트,Mettre à jour +Check for Updates,Проверить обновления,检查更新,檢查更新,アップデートを確認,업데이트 확인하기,Vérifier les mises à jour +About,О программе,关于,關於,shadowsocks-reborn について,정보,A propos +Help,Помощь,帮助,說明,ヘルプ,도움말,Aide +Quit,Выход,退出,結束,終了,종료,Quitter +Load Balance,Балансировка нагрузки,负载均衡,負載平衡,サーバーロードバランス,로드밸런싱,Répartition de charge +High Availability,Высокая доступность,高可用,高可用性,高可用性,고가용성,Haute disponibilité +Show Plugin Output,События плагинов в журнале,显示插件输出,顯示外掛程式輸出,プラグインの出力情報を表示,플러그인 출력 보이기,Afficher la sortie du plugin +# Config Form,,,,,, +Statistics configuration,Настройки статистики,统计配置,統計設定,統計の設定,통계 설정,Configuration des statistiques +&Add,Добавить,添加(&A),新增 (&A),新規 (&A),추가 (&A),Ajouter +&Delete,Удалить,删除(&D),移除 (&D),削除 (&D),삭제 (&D),Supprimer +Dupli&cate,Дублир-ть,复制(&C),複製 (&C),コピー (&C),복제 (&C),Dupliquer +Server,Сервер,服务器,伺服器,サーバー,서버,Serveur +Server IP,IP-адрес,服务器地址,伺服器位址,サーバーアドレス,서버 IP,Serveur IP +Server Port,Порт,服务器端口,伺服器連接埠,サーバーポート,서버 포트,Port de serveur +Password,Пароль,密码,密碼,パスワード,비밀번호,Mot de passe +Show Password,Показать пароль,显示密码,顯示密碼,パスワードを表示する,비밀번호 보이기,Montrer le mot de passe +Encryption,Шифрование,加密,加密,暗号化,암호화,Chiffrement +Plugin Program,Плагин,插件程序,外掛程式,プラグインプログラム,플러그인 프로그램,Programme de plugin +Plugin Options,Опции плагина,插件选项,外掛程式選項,プラグインのオプション,플러그인 설정,Options de plugin +Environment variables for plugin program,Переменные окружения для программы плагина,插件程序的环境变量,外掛程式的環境變數,プラグインプログラムの環境変数,플러그인 프로그램의 환경 변수,Variables d’environnement du programme de plugin +Need Plugin Argument,Требуются аргументы,需要命令行参数,需要外掛程式參數,コマンドライン引数を有効にする,플러그인 인자가 필요함,Besoin d'un argument de plugin +Plugin Arguments,Аргументы,插件参数,外掛程式參數,プラグインの引数,플러그인 인자,Arguments du plugin +"Plugin arguments are not part of SIP003. They are passed as command-line arguments. Mandatory: %SS_LOCAL_HOST%, %SS_LOCAL_PORT%, %SS_REMOTE_HOST%, %SS_REMOTE_PORT%. Optional: %SS_PLUGIN_OPTIONS%.","Аргументы плагина не входят в стандарт SIP003 и передаются через командную строку. Обязательные: %SS_LOCAL_HOST%, %SS_LOCAL_PORT%, %SS_REMOTE_HOST%, %SS_REMOTE_PORT%. Необязательная: %SS_PLUGIN_OPTIONS%.",插件参数不属于 SIP003,它们作为命令行参数传递。必需:%SS_LOCAL_HOST%、%SS_LOCAL_PORT%、%SS_REMOTE_HOST%、%SS_REMOTE_PORT%。可选:%SS_PLUGIN_OPTIONS%。,外掛程式參數不屬於 SIP003,而是以命令列參數傳遞。必要:%SS_LOCAL_HOST%、%SS_LOCAL_PORT%、%SS_REMOTE_HOST%、%SS_REMOTE_PORT%。選用:%SS_PLUGIN_OPTIONS%。,プラグイン引数は SIP003 の一部ではなく、コマンドライン引数として渡されます。必須: %SS_LOCAL_HOST%、%SS_LOCAL_PORT%、%SS_REMOTE_HOST%、%SS_REMOTE_PORT%。任意: %SS_PLUGIN_OPTIONS%。,"플러그인 인수는 SIP003의 일부가 아니며 명령줄 인수로 전달됩니다. 필수: %SS_LOCAL_HOST%, %SS_LOCAL_PORT%, %SS_REMOTE_HOST%, %SS_REMOTE_PORT%. 선택: %SS_PLUGIN_OPTIONS%.","Les arguments de plugin ne font pas partie de SIP003. Ils sont transmis en ligne de commande. Obligatoires : %SS_LOCAL_HOST%, %SS_LOCAL_PORT%, %SS_REMOTE_HOST%, %SS_REMOTE_PORT%. Facultatif : %SS_PLUGIN_OPTIONS%." +Proxy Port,Порт прокси,代理端口,Proxy 連接埠,プロキシポート,프록시 포트,Port proxy +Portable Mode,Переносимый режим,便携模式,便攜模式,ポータブルモード,포터블 모드,Mode portable +Restart required,Требуется перезапуск программы,需要重新启动SS,需要重新啟動SS,再起動が必要,재시작이 필요합니다,Redémarrage nécessaire +Remarks,Примечания,备注,註解,付記,알림,Remarques +Group,Группа,组,群組,グループ,그룹,Groupe +Timeout(Sec),Таймаут(сек),超时(秒),逾時 (秒),タイムアウト (秒),시간 초과 (초),Délai d'attente(sec) +OK,ОК,确定,確定,OK,확인,OK +Cancel,Отмена,取消,取消,キャンセル,취소,Annuler +Close,Закрыть,关闭,關閉,閉じる,닫기,Fermer +Apply,Применить,应用,應用,適用,적용,Appliquer +New server,Новый сервер,未配置的服务器,新伺服器,新規サーバー,새 서버,Nouveau serveur +Move &Up,Выше,上移(&U),上移 (&U),上に移動 (&U),위로 (&U),Monter +Move D&own,Ниже,下移(&O),下移 (&O),下に移動 (&O),아래로 (&O),Descendre +deprecated,Устаревшее,不推荐,不推薦,非推奨,더 이상 사용되지 않음,Obsolète +"Encryption method {0} not exist, will replace with {1}",Метод шифрования {0} не найден; будет использован {1},加密方法{0}不存在,将使用{1}代替,加密方式 {0} 不存在,將替換為 {1},暗号化方式{0}が存在しません,{1}に置換します,{0} 암호화 방식이 존재하지 않으므로 {1}로 대체될 것입니다.,"Méthode de chiffrement {0} n'existe pas, sera remplacée par {1}" +# Log Form,,,,,, +&File,Файл,文件(&F),檔案 (&F),ファイル (&F),파일 (&F),Fichier +&Open Location,Расположение файла,在资源管理器中打开(&O),在檔案總管中開啟 (&O),ファイルの場所を開く (&O),위치 열기 (&O),Ouvrier +E&xit,Выход,退出(&X),結束 (&X),終了 (&X),종료 (&X),Quitter +&View,Вид,视图(&V),檢視 (&V),表示 (&V),보기 (&V),Afficher +&Clear Logs,Очистить журнал,清空日志(&C),清除記錄檔 (&C),ログの削除 (&C),로그 초기화 (&C),Effacer les journaux +Change &Font,Шрифт…,设置字体(&F),變更字型 (&F),フォント (&F),글꼴 변경 (&F),Définir la police +&Font,Шрифт,字体(&F),字型(&F),フォント(&F),글꼴(&F),&Police +&Wrap Text,Перенос строк,自动换行(&W),自動換行 (&W),右端で折り返す (&W),텍스트 감싸기 (&W),Retour à la ligne +&Top Most,Поверх всех окон,置顶(&T),置頂 (&T),常に最前面に表示 (&T),상단 우선 (Top Most) (&T),Metter en haut +&Show Toolbar,Панель инструментов,显示工具栏(&S),顯示工具列 (&S),ツールバーの表示 (&S),툴바 보여주기 (&S),Afficher la barre d'outils +Log Viewer,Просмотр журнала,日志查看器,記錄檔檢視器,ログビューア,로그 뷰어,Visionneuse de journaux +Inbound,Входящая,入站,輸入,受信,"인바운드 (Rx, 다운로드)",Entrant +Outbound,Исходящая,出站,輸出,送信,"아웃바운드 (Tx, 업로드)",Sortant +In,Вх.,入,入,受信,수신,Entrée +Out,Исх.,出,出,送信,송신,Sortie +# QRCode Form,,,,,, +QRCode and URL,QRCode и URL,二维码与 URL,QR 碼與 URL,QR コードと URL,QR코드와 URL,QRCode et URL +# PAC Url Form,,,,,, +Edit Online PAC URL,Изменение URL удаленного PAC,编辑在线 PAC 网址,編輯線上 PAC 網址,オンライン PAC URL の編集,온라인 프록시 자동 구성 URL 수정,Modifier l'URL du PAC en ligne +Please input PAC Url,Введите URL адрес для PAC-файла,请输入 PAC 网址,請輸入 PAC 網址,PAC URLを入力して下さい,프록시 자동 구성 URL을 입력하세요,Veuillez saisir l'URL PAC +# Messages,,,,,, +shadowsocks-reborn Error: {0},Ошибка shadowsocks-reborn: {0},shadowsocks-reborn 错误: {0},shadowsocks-reborn 錯誤: {0},shadowsocks-reborn エラー: {0},shadowsocks-reborn 오류: {0},Erreur shadowsocks-reborn: {0} +Port {0} already in use,Порт {0} уже используется,端口 {0} 已被占用,連接埠號碼 {0} 已被使用,ポート番号 {0} は既に使用されています。,{0}번 포트는 이미 사용 중입니다.,Port {0} déjà utilisé +Port {0} is reserved by system,Порт {0} зарезервирован системой,端口 {0} 是系统保留端口,連接埠號碼 {0} 由系統保留, ポート番号 {0} はシステムによって予約されています,{0}번 포트는 시스템에서 사용 중입니다.,Port {0} réservé par le système +Invalid server address,Неверный адрес сервера,非法服务器地址,無效伺服器位址,サーバーアドレスが無効です。,올바르지 않은 서버 주소입니다.,Adresse de serveur non valide +Illegal port number format,Неверный числовой формат порта,非法端口格式,無效連接埠號碼格式,ポート番号のフォーマットが無効です。,올바르지 않은 포트 번호 형식입니다.,Format de numéro de port illégal +Illegal timeout format,Неверный формат таймаута,非法超时格式,無效逾時格式,タイムアウト値のフォーマットが無効です。,올바르지 않은 시간 초과 형식입니다.,Format de délai d'attente illégal +Server IP can not be blank,IP-адрес сервера не может быть пустым,服务器 IP 不能为空,伺服器 IP 不能為空,サーバー IP が指定されていません。,서버 IP는 비어있으면 안됩니다.,L'adresse IP du serveur ne peut pas être vide +Password can not be blank,Пароль не может быть пустым,密码不能为空,密碼不能為空,パスワードが指定されていません。,비밀번호는 비어있으면 안됩니다.,Le mot de passe ne peut pas être vide +Port out of range,Порт выходит за допустимый диапазон,端口超出范围,連接埠號碼超出範圍,ポート番号は範囲外です。,올바른 포트 범위가 아닙니다.,Port hors de portée +Port can't be 8123,Адрес порта 8123 не может быть использован,端口不能为 8123,連接埠號碼不能為 8123,8123 番以外のポート番号を指定して下さい。,8123번 포트는 사용할 수 없습니다.,Le port ne peut pas être 8123 +No update is available,Обновлений не обнаружено,没有可用的更新,沒有可用的更新,お使いのバージョンは最新です。,사용 가능한 업데이트가 없습니다.,Aucune mise à jour n'est disponible +shadowsocks-reborn is here,shadowsocks-reborn находится здесь,shadowsocks-reborn 在这里,shadowsocks-reborn 在這裡,shadowsocks-reborn はここです。,shadowsocks-reborn는 여기에 있습니다,Veuillez trouver shadowsocks-reborn ici +You can turn on/off shadowsocks-reborn in the context menu,Вы можете управлять shadowsocks-reborn из контекстного меню,可以在右键菜单中开关 shadowsocks-reborn,可以在右鍵選項單中開關 shadowsocks-reborn,コンテキストメニューを使って、shadowsocks-reborn を有効または無効にすることができます。,프로그램 메뉴에서 shadowsocks-reborn를 끄고 켤 수 있습니다.,Vous pouvez activer / désactiver shadowsocks-reborn dans le menu contextuel +System Proxy Enabled,Системный прокси включен,系统代理已启用,系統 Proxy 已啟用,システム プロキシが有効です。,시스템 프록시가 활성화되었습니다.,Proxy système activé +System Proxy Disabled,Системный прокси отключен,系统代理未启用,系統 Proxy 未啟用,システム プロキシが無効です。,시스템 프록시가 비활성화되었습니다.,Proxy système désactivé +Failed to update PAC file,Не удалось обновить PAC файл,更新 PAC 文件失败,更新 PAC 檔案失敗,PAC の更新に失敗しました。,프록시 자동 구성 파일을 업데이트하는데 실패했습니다.,Impossible de mettre à jour le fichier PAC +PAC updated,PAC файл обновлен,更新 PAC 成功,更新 PAC 成功,PAC を更新しました。,프록시 자동 구성 파일이 업데이트되었습니다.,PAC mis à jour +No updates found. Please report to Geosite if you have problems with it.,Обновлений не найдено. Сообщите авторам Geosite если у вас возникли проблемы.,未发现更新。如有问题请提交给 Geosite。,未發現更新。如有問題請報告至 Geosite。,お使いのバージョンは最新です。問題がある場合は、GFWList に報告して下さい。,사용 가능한 업데이트를 찾지 못했습니다. 문제가 있다면 Geosite로 전송해주세요.,Aucune mise à jour trouvée. Veuillez signaler à Geosite si vous avez des problèmes concernant. +No QRCode found. Try to zoom in or move it to the center of the screen.,QRCode не обнаружен. Попробуйте увеличить изображение или переместить его в центр экрана.,未发现二维码,尝试把它放大或移动到靠近屏幕中间的位置,未發現 QR 碼,嘗試把它放大或移動到靠近熒幕中間的位置,QR コードが見つかりませんでした。コードを大きくするか、画面の中央に移動して下さい。,QR코드를 찾을 수 없습니다. 가운데로 화면을 이동시키거나 확대해보세요.,Aucun QRCode trouvé. Essayez de zoomer ou de le déplacer vers le centre de l'écran. +Shadowsocks Reborn is already running,Shadowsocks Reborn уже запущен,Shadowsocks Reborn 已在运行,Shadowsocks Reborn 已在執行,Shadowsocks Reborn はすでに実行中です,Shadowsocks Reborn이 이미 실행 중입니다,Shadowsocks Reborn est déjà en cours d’exécution +The existing instance has been brought to the foreground.,Уже запущенный экземпляр выведен на передний план.,已将正在运行的实例切换到前台。,已將正在執行的執行個體切換到前景。,実行中のインスタンスを前面に表示しました。,실행 중인 인스턴스를 앞으로 가져왔습니다.,L’instance déjà en cours d’exécution a été placée au premier plan. +"If you want to start multiple shadowsocks-reborn, make a copy in another directory.","Если вы хотите запустить несколько копий shadowsocks-reborn одновременно, создайте отдельную папку на каждую копию.",如果想同时启动多个,可以另外复制一份到别的目录。,如果想同時啟動多個,可以另外複製一份至別的目錄。,複数起動したい場合は、プログラムファイルを別のフォルダーにコピーしてから、もう一度実行して下さい。,"만약 여러 개의 shadowsocks-reborn를 사용하고 싶으시다면, 파일을 다른 곳에 복사해주세요.","Si vous souhaitez démarrer plusieurs shadowsocks-reborn, faites une copie dans un autre répertoire." +Invalid QR Code content: {0},Недопустимое содержимое QR-кода: {0},无效二维码内容: {0},無效二維碼內容: {0},無効な QR コード内容: {0},잘못된 QR 코드 내용: {0},Contenu du QR code invalide : {0} +Failed to update registry,Не удалось обновить запись в реестре,无法修改注册表,無法修改登錄檔,レジストリの更新に失敗しました。,레지스트리를 업데이트하는데에 실패했습니다.,Impossible de mettre à jour de la base de registre +Failed to open registry key: {0},Не удалось открыть раздел реестра: {0},无法打开注册表项:{0},無法開啟登錄機碼:{0},レジストリキーを開けませんでした: {0},레지스트리 키를 열 수 없습니다: {0},Impossible d’ouvrir la clé de Registre : {0} +Import from URL: {0} ?,импортировать из адреса: {0} ?,从URL导入: {0} ?,從URL匯入: {0} ?,{0}:このURLからインポートしますか?,URL에서 가져오기: {0} ?,Importer depuis l’URL : {0} ? +Successfully imported from {0},Успешно импортировано из {0},导入成功:{0},導入成功:{0},{0}:インポートしました。,{0}에서 성공적으로 가져왔습니다,Importation réussie depuis {0} +Failed to import. Please check if the link is valid.,Не удалось импортировать. Проверьте корректность ссылки.,导入失败,请检查链接是否有效。,導入失敗,請檢查鏈接是否有效。,インポートに失敗しました。リンクの有効性を確認してください。,가져오기에 실패했습니다. 링크가 유효한지 확인하세요.,Échec de l’importation. Vérifiez que le lien est valide. +System Proxy On:,Системный прокси:,系统代理已启用:,系統 Proxy 已啟用:,システム プロキシが有効:,시스템 프록시 활성화됨: ,Proxy système activé: +Running: Port {0},Запущен на порту {0},正在运行:端口 {0},正在執行:連接埠號碼 {0},実行中:ポート {0},실행 중: 포트 {0}번,En cours d'exécution: port {0} +"Unexpected error, shadowsocks-reborn will exit. Please report to","Непредвиденная ошибка, пожалуйста сообщите на",非预期错误,shadowsocks-reborn将退出。请提交此错误到,非預期錯誤,shadowsocks-reborn 將結束。請報告此錯誤至,予想外のエラーが発生したため、shadowsocks-reborn を終了します。詳しくは下記までお問い合わせ下さい:,알 수 없는 오류로 shadowsocks-reborn가 종료될 것입니다. 오류를 여기로 제보해주세요:,shadowsocks-reborn va quitter en présence d/érreur inattendue. Veuillez signaler à +Unsupported operating system. Windows 10 version 2004 (build 19041) or newer is required.,Операционная система не поддерживается. Требуется Windows 10 версии 2004 (сборка 19041) или новее.,需要 Windows 10 版本 2004(内部版本 19041)或更高版本。,需要 Windows 10 版本 2004(組建 19041)或更新版本。,Windows 10 バージョン 2004(ビルド 19041)以降が必要です.,Windows 10 버전 2004(빌드 19041) 이상이 필요합니다.,Windows 10 version 2004 (build 19041) ou une version ultérieure est requis. +Proxy request failed,Не удалось выполнить запрос,代理请求失败,Proxy 要求失敗,プロキシリクエストが失敗しました。,프록시 요청에 실패했습니다.,Échec de la demande de proxy +Proxy handshake failed,Не удалось выполнить хэндшейк,代理握手失败,Proxy 交握失敗,プロキシ ハンドシェイクに失敗しました。,프록시 핸드쉐이크에 실패했습니다.,Échec de la prise de contact par proxy +Register hotkey failed,Не удалось применить настройки горячих клавиш,注册快捷键失败,註冊快速鍵失敗,ホットキーの登錄に失敗しました。,단축키 등록에 실패했습니다.,Échec de l'enregistrement du raccourci clavier +Cannot parse hotkey: {0},Не удалось распознать следующие горячие клавиши: {0},解析快捷键失败: {0},剖析快速鍵失敗: {0},ホットキーを解析できません: {0},단축키를 해석할 수 없습니다: {0},Impossible d'analyser le raccourci clavier: {0} +"Timeout is invalid, it should not exceed {0}",Таймаут не может превышать значение {0},超时无效,不应超过 {0},逾時無效,不應超過 {0},タイムアウト値が無効です。{0} 以下の値を指定して下さい。,올바르지 않은 시간 초과 설정입니다. {0}을 초과하지 않아야 합니다.,"Le délai d'attente invalide, il ne doit pas dépasser {0}" +Cannot find the plugin program file,Файл плагина не найден,找不到插件程序文件,找不到外掛程式文件,プラグインプログラムが見つかりません,플러그인 프로그램 파일을 찾을 수 없습니다.,Impossible de trouver le fichier du programme du plugin +Operation failure,Операция завершилась неудачей,操作失败,操作失敗,操作に失敗しました,작업에 실패했습니다.,Échec de l’opération +Auto save failed,Автоматическое сохранение не удалось,自动保存失败,自動儲存失敗,オートセーブに失敗しました,자동 저장에 실패했습니다.,Échec de l'enregistrement automatique +Whether to discard unconfigured servers,Внесенные изменения будут утеряны,是否丢弃未配置的服务器,是否捨棄未設定的伺服器,未設定のサーバーを破棄しますか,구성되지 않은 서버 설정 변경 사항을 폐기하시겠습니까?,Eliminer les serveurs non configurés ou non +"Invalid server address, Cannot automatically save or discard changes",Неверный адрес сервера. Невозможно сохранить или отменить изменения,非法服务器地址,无法自动保存,是否丢弃修改,伺服器位址無效,無法自動儲存或捨棄變更,サーバーアドレスが無効なため、オートセーブできません。変更を破棄しますか,"서버 주소가 올바르지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Adresse de serveur invalide, impossible d'enregistrer ou d'annuler automatiquement les modifications" +"Illegal port number format, Cannot automatically save or discard changes",Неверный числовой адрес порта. Невозможно сохранить или отменить изменения,非法端口格式,无法自动保存,是否丢弃修改,連接埠號格式無效,無法自動儲存或捨棄變更,ポート番号のフォーマットが無効なため、オートセーブできません。変更を破棄しますか,"포트 번호가 올바르지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Format de numéro de port illégal, impossible d'enregistrer ou d'annuler automatiquement les modifications" +"Password can not be blank, Cannot automatically save or discard changes",Пароль не может быть пустым. Невозможно сохранить или отменить изменения,密码不能为空,无法自动保存,是否丢弃修改,密碼不能為空白,無法自動儲存或捨棄變更,サーバーアドレスが無効なため、オートセーブできません。変更を破棄しますか,"비밀번호를 입력하지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Le mot de passe ne peut pas être vide, impossible d'enregistrer ou d'annuler automatiquement les modifications" +"Illegal timeout format, Cannot automatically save or discard changes",Неверный формат таймаута. Невозможно сохранить или отменить изменения,非法超时格式,无法自动保存,是否丢弃修改,逾時格式無效,無法自動儲存或捨棄變更,パスワードが指定されていないため、オートセーブできません。変更を破棄しますか,시간 초과 형식이 올바르지 않아 자동 저장 할 수 없습니다. 변경 사항을 폐기하시겠습니까?,"Format de délai d'attente illégal, impossible d'enregistrer ou d'annuler automatiquement les modifications" +"Error occured when process proxy setting, do you want reset current setting and retry?",Произошла ошибка при обработке настроек. Хотите сбросить текущие настройки и попробовать снова?,处理代理设置时发生错误,是否重置当前代理设置并重试?,處理 Proxy 設定時發生錯誤,是否重設目前設定並重試?,プロキシ設定の処理にエラーが発生しました、現在のプロキシ設定をリセットし、再試行してもいいですか,프록시 설정을 처리하는데에 오류가 발생했습니다. 현재 설정을 폐기하고 다시 시도하시겠습니까?,Une erreur s'est produite lors du processus de configuration du proxy. Voulez-vous réinitialiser le paramètre actuel et réessayer? +"Unrecoverable proxy setting error occured, see log for detail","Произошла серьезная ошибка, подробности можно узнать в журналах",发生不可恢复的代理设置错误,查看日志以取得详情,發生無法復原的 Proxy 設定錯誤,詳細資料請參閱記錄,プロキシ設定に回復不能なエラーが発生しました、ログで詳細をご確認ください,복구 불가능한 프록시 설정 오류가 발생했습니다. 자세한 정보는 로그를 참조하세요.,"Une erreur de paramètre de proxy irrécupérable s'est produite, consultez le journal pour plus de détails" +Traffic Mode,Режим трафика,流量模式,流量模式,トラフィックモード,트래픽 모드,Mode trafic +User Mode,Пользовательский режим,用户模式,使用者模式,ユーザーモード,사용자 모드,Mode utilisateur +Admin Mode,Режим администратора,管理员模式,系統管理員模式,管理者モード,관리자 모드,Mode administrateur +Traffic Routing,Маршрутизация трафика,流量路由,流量路由,トラフィックルーティング,트래픽 라우팅,Routage du trafic +WinDivert: active,WinDivert: активен,WinDivert:已启用,WinDivert:已啟用,WinDivert: 有効,WinDivert: 활성,WinDivert : actif +WinDivert: paused (game running),WinDivert: приостановлен (игра запущена),WinDivert:已暂停(游戏运行中),WinDivert:已暫停(遊戲執行中),WinDivert: 一時停止(ゲーム実行中),WinDivert: 일시 중지됨 (게임 실행 중),WinDivert : suspendu (jeu en cours) +WinDivert: inactive,WinDivert: неактивен,WinDivert:未启用,WinDivert:未啟用,WinDivert: 無効,WinDivert: 비활성,WinDivert : inactif +.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64,.NET 10 · WinUI 3 · Windows App SDK · x64 +About & updates,О программе и обновления,关于和更新,關於與更新,バージョン情報と更新,정보 및 업데이트,À propos et mises à jour +Action,Действие,操作,動作,操作,작업,Action +Activation,Активация,激活,啟用,アクティベーション,활성화,Activation +Active,Активен,活动,啟用中,アクティブ,활성,Actif +ACTIVE,АКТИВЕН,活动,啟用中,アクティブ,활성,ACTIF +Add,Добавить,添加,新增,追加,추가,Ajouter +Add application,Добавить приложение,添加应用程序,新增應用程式,アプリケーションを追加,애플리케이션 추가,Ajouter une application +Add source URL(s),Добавить URL источников,添加源 URL,新增來源 URL,ソース URL を追加,소스 URL 추가,Ajouter des URL sources +Admin capture is suspended automatically while a configured anti-cheat-sensitive application is running.,"Захват в режиме администратора автоматически приостанавливается, пока запущено настроенное приложение, чувствительное к античиту.",运行已配置的反作弊敏感应用程序时,将自动暂停管理员捕获。,執行已設定的反作弊敏感應用程式時,會自動暫停系統管理員擷取。,設定済みのアンチチートに敏感なアプリケーションの実行中は、管理者キャプチャが自動的に停止します。,설정된 안티치트 민감 애플리케이션이 실행 중이면 관리자 캡처가 자동으로 일시 중지됩니다.,La capture administrateur est automatiquement suspendue lorsqu’une application configurée sensible à l’anti-triche est en cours d’exécution. +Administrator capture status,Состояние захвата администратора,管理员捕获状态,系統管理員擷取狀態,管理者キャプチャの状態,관리자 캡처 상태,État de la capture administrateur +All configured shortcuts registered.,Все настроенные горячие клавиши зарегистрированы.,已注册所有已配置的快捷键。,已註冊所有已設定的快速鍵。,設定済みのショートカットをすべて登録しました。,구성된 모든 바로 가기를 등록했습니다.,Tous les raccourcis configurés ont été enregistrés. +All online configurations updated.,Все онлайн-конфигурации обновлены.,已更新所有在线配置。,已更新所有線上設定。,すべてのオンライン設定を更新しました。,모든 온라인 구성을 업데이트했습니다.,Toutes les configurations en ligne ont été mises à jour. +App theme,Тема приложения,应用主题,應用程式主題,アプリのテーマ,앱 테마,Thème de l’application +Appearance,Оформление,外观,外觀,外観,모양,Apparence +Application,Приложение,应用程序,應用程式,アプリケーション,애플리케이션,Application +Application preferences use native Windows controls and follow the Windows 11 settings layout.,Параметры приложения используют нативные элементы Windows и компоновку настроек Windows 11.,应用首选项使用原生 Windows 控件,并遵循 Windows 11 设置布局。,應用程式偏好設定使用原生 Windows 控制項,並遵循 Windows 11 設定版面配置。,アプリ設定にはネイティブ Windows コントロールを使用し、Windows 11 の設定レイアウトに従います。,애플리케이션 환경설정은 네이티브 Windows 컨트롤을 사용하며 Windows 11 설정 레이아웃을 따릅니다.,Les préférences de l’application utilisent les contrôles Windows natifs et suivent la disposition des paramètres de Windows 11. +Application routing,Маршрутизация приложений,应用程序路由,應用程式路由,アプリケーションルーティング,애플리케이션 라우팅,Routage des applications +Applications,Приложения,应用程序,應用程式,アプリケーション,애플리케이션,Applications +Apply port,Применить порт,应用端口,套用連接埠,ポートを適用,포트 적용,Appliquer le port +At least one GeoSite source is required.,Необходимо указать хотя бы один источник GeoSite.,至少需要一个 GeoSite 源。,至少需要一個 GeoSite 來源。,少なくとも1つの GeoSite ソースが必要です。,GeoSite 소스가 하나 이상 필요합니다.,Au moins une source GeoSite est requise. +Auto-refresh,Автообновление,自动刷新,自動重新整理,自動更新,자동 새로 고침,Actualisation automatique +Automatic Game Mode application list saved.,Список приложений автоматического игрового режима сохранён.,自动游戏模式应用程序列表已保存。,自動遊戲模式應用程式清單已儲存。,自動 Game Mode のアプリケーション一覧を保存しました。,자동 Game Mode 애플리케이션 목록을 저장했습니다.,La liste des applications du Game Mode automatique a été enregistrée. +Background mode,Фоновый режим,后台模式,背景模式,バックグラウンドモード,백그라운드 모드,Mode arrière-plan +Behavior,Поведение,行为,行為,動作,동작,Comportement +Logging,Журналирование,日志,記錄,ログ,로깅,Journalisation +Block,Блокировать,阻止,封鎖,ブロック,차단,Bloquer +Bold,Полужирный,粗体,粗體,太字,굵게,Gras +Capture mode,Режим захвата,捕获模式,擷取模式,キャプチャモード,캡처 모드,Mode de capture +Checking GitHub releases…,Проверка релизов GitHub…,正在检查 GitHub 版本…,正在檢查 GitHub 版本…,GitHub のリリースを確認しています…,GitHub 릴리스를 확인하는 중…,Vérification des versions GitHub… +Choose Local GeoSite PAC or an Online PAC and manage the ordered GeoSite source list.,Выберите локальный PAC на основе GeoSite или онлайн-PAC и управляйте упорядоченным списком источников GeoSite.,选择本地 GeoSite PAC 或在线 PAC,并管理有序的 GeoSite 源列表。,選擇本機 GeoSite PAC 或線上 PAC,並管理已排序的 GeoSite 來源清單。,ローカル GeoSite PAC またはオンライン PAC を選択し、順序付きの GeoSite ソース一覧を管理します。,로컬 GeoSite PAC 또는 온라인 PAC를 선택하고 정렬된 GeoSite 소스 목록을 관리합니다.,Choisir le PAC GeoSite local ou un PAC en ligne et gérer la liste ordonnée des sources GeoSite. +"Choose User or Administrator capture, inspect WinDivert state, and route individual applications.","Выберите пользовательский режим или режим администратора, контролируйте состояние WinDivert и задавайте маршрутизацию отдельных приложений.",选择用户或管理员捕获,查看 WinDivert 状态,并按应用程序路由。,選擇使用者或系統管理員擷取、檢視 WinDivert 狀態,並依應用程式路由。,ユーザーまたは管理者キャプチャを選択し、WinDivert の状態を確認してアプリケーションごとにルーティングします。,사용자 또는 관리자 캡처를 선택하고 WinDivert 상태를 확인하며 애플리케이션별로 라우팅합니다.,"Choisir la capture utilisateur ou administrateur, inspecter l’état de WinDivert et router les applications individuellement." +Clear logs,Очистить логи,清除日志,清除記錄,ログを消去,로그 지우기,Effacer les journaux +Clipboard does not contain an ss:// URL,Буфер обмена не содержит URL ss://,剪贴板不包含 ss:// URL,剪貼簿不包含 ss:// URL,クリップボードに ss:// URL がありません,클립보드에 ss:// URL이 없습니다,Le Presse-papiers ne contient pas d’URL ss:// +Clipboard does not contain text,Буфер обмена не содержит текста,剪贴板不包含文本,剪貼簿不包含文字,クリップボードにテキストがありません,클립보드에 텍스트가 없습니다,Le Presse-papiers ne contient pas de texte +Closing the window hides it to the notification area. The proxy controller keeps running until you choose Exit from the tray menu.,"Закрытие окна скрывает его в область уведомлений. Контроллер прокси продолжает работать, пока вы не выберете выход в меню трея.",关闭窗口会将其隐藏到通知区域。代理控制器会继续运行,直到从托盘菜单选择“退出”。,關閉視窗會將其隱藏到通知區域。Proxy 控制器會繼續執行,直到從系統匣選單選擇「結束」。,ウィンドウを閉じると通知領域に隠れます。トレイメニューから「終了」を選ぶまでプロキシコントローラーは動作を続けます。,창을 닫으면 알림 영역으로 숨겨집니다. 트레이 메뉴에서 종료를 선택할 때까지 프록시 컨트롤러는 계속 실행됩니다.,La fermeture de la fenêtre la masque dans la zone de notification. Le contrôleur proxy continue de fonctionner jusqu’à ce que vous choisissiez Quitter dans le menu de la zone de notification. +Configure global keyboard shortcuts.,Настройте глобальные сочетания клавиш.,配置全局键盘快捷键。,設定全域鍵盤快速鍵。,グローバルキーボードショートカットを設定します。,전역 키보드 단축키를 구성합니다.,Configurer les raccourcis clavier globaux. +Connect to the Shadowsocks server through an optional upstream proxy.,Подключайтесь к серверу Shadowsocks через необязательный внешний прокси.,通过可选的上游代理连接 Shadowsocks 服务器。,透過選用的上游 Proxy 連線 Shadowsocks 伺服器。,任意の上流プロキシ経由で Shadowsocks サーバーに接続します。,선택적 업스트림 프록시를 통해 Shadowsocks 서버에 연결합니다.,Se connecter au serveur Shadowsocks via un proxy amont facultatif. +Connection state,Состояние подключения,连接状态,連線狀態,接続状態,연결 상태,État de la connexion +Controller,Контроллер,控制器,控制器,コントローラー,컨트롤러,Contrôleur +Controller is unavailable.,Контроллер недоступен.,控制器不可用。,控制器無法使用。,コントローラーを利用できません。,컨트롤러를 사용할 수 없습니다.,Le contrôleur est indisponible. +Controller unavailable,Контроллер недоступен,控制器不可用,控制器無法使用,コントローラー利用不可,컨트롤러 사용 불가,Contrôleur indisponible +Copy,Копировать,复制,複製,コピー,복사,Copier +Copy link,Копировать ссылку,复制链接,複製連結,リンクをコピー,링크 복사,Copier le lien +"Core networking is UI-independent. Windows integration, WinDivert coordination and tray support live in the Windows platform layer.","Сетевое ядро не зависит от интерфейса. Интеграция с Windows, координация WinDivert и поддержка трея находятся в платформенном слое Windows.",核心网络逻辑独立于 UI。Windows 集成、WinDivert 协调和托盘支持位于 Windows 平台层。,核心網路邏輯獨立於 UI。Windows 整合、WinDivert 協調與系統匣支援位於 Windows 平台層。,コアネットワークは UI から独立しています。Windows 統合、WinDivert の制御、トレイ対応は Windows プラットフォーム層にあります。,"핵심 네트워킹은 UI와 독립적입니다. Windows 통합, WinDivert 조정 및 트레이 지원은 Windows 플랫폼 계층에 있습니다.","Le réseau principal est indépendant de l’interface. L’intégration Windows, la coordination WinDivert et la prise en charge de la zone de notification résident dans la couche de plateforme Windows." +Credentials (optional),Учётные данные (необязательно),凭据(可选),認證資訊(選用),認証情報(任意),자격 증명(선택 사항),Identifiants (facultatif) +Ctrl+Alt+S,Ctrl+Alt+S,Ctrl+Alt+S,Ctrl+Alt+S,Ctrl+Alt+S,Ctrl+Alt+S,Ctrl+Alt+S +"Current proxy, server, and system-shell state at a glance.","Краткое состояние прокси, сервера и системной оболочки.",一览当前代理、服务器和系统 shell 状态。,快速檢視目前的 Proxy、伺服器與系統 Shell 狀態。,現在のプロキシ、サーバー、システムシェルの状態を一覧表示します。,"현재 프록시, 서버 및 시스템 셸 상태를 한눈에 확인합니다.","Afficher d’un coup d’œil l’état actuel du proxy, du serveur et du shell système." +Current state,Текущее состояние,当前状态,目前狀態,現在の状態,현재 상태,État actuel +Dark,Тёмная,深色,深色,ダーク,어둡게,Sombre +Debug,Отладка,调试,偵錯,デバッグ,디버그,Débogage +Details,Параметры,详细信息,詳細資料,詳細,세부 정보,Détails +Direct,Напрямую,直连,直接連線,直接接続,직접 연결,Direct +Disabled,Отключён,已禁用,已停用,無効,비활성화됨,Désactivé +Discard,Отбросить,丢弃,捨棄,破棄,버리기,Ignorer +Discard changes,Отменить изменения,丢弃更改,捨棄變更,変更を破棄,변경 내용 버리기,Ignorer les modifications +Download / refresh GeoSite,Скачать / обновить GeoSite,下载 / 刷新 GeoSite,下載 / 重新整理 GeoSite,GeoSite をダウンロード / 更新,GeoSite 다운로드 / 새로 고침,Télécharger / actualiser GeoSite +Enabled,Включено,已启用,已啟用,有効,활성화됨,Activé +Enabled · Global,Включён · Глобальный,已启用 · 全局,已啟用 · 全域,有効 · グローバル,활성화됨 · 글로벌,Activé · Global +Enabled · PAC,Включён · PAC,已启用 · PAC,已啟用 · PAC,有効 · PAC,활성화됨 · PAC,Activé · PAC +Enter an absolute HTTP or HTTPS URL.,Введите абсолютный HTTP- или HTTPS-URL.,输入绝对 HTTP 或 HTTPS URL。,輸入絕對 HTTP 或 HTTPS URL。,絶対 HTTP または HTTPS URL を入力します。,절대 HTTP 또는 HTTPS URL을 입력합니다.,Saisir une URL absolue HTTP ou HTTPS. +Every 2 seconds,Каждые 2 секунды,每 2 秒,每 2 秒,2秒ごと,2초마다,Toutes les 2 secondes +Every source must be an absolute HTTP or HTTPS URL.,Каждый источник должен быть абсолютным HTTP- или HTTPS-URL.,每个源都必须是绝对 HTTP 或 HTTPS URL。,每個來源都必須是絕對 HTTP 或 HTTPS URL。,各ソースは絶対 HTTP または HTTPS URL である必要があります。,각 소스는 절대 HTTP 또는 HTTPS URL이어야 합니다.,Chaque source doit être une URL absolue HTTP ou HTTPS. +Failed sources: {0},Не удалось обновить источники: {0},失败的源:{0},失敗的來源:{0},失敗したソース: {0},실패한 소스: {0},Sources en échec : {0} +Failed to open {0}: {1},Не удалось открыть {0}: {1},无法打开 {0}:{1},無法開啟 {0}:{1},{0} を開けませんでした: {1},{0}을(를) 열 수 없습니다: {1},Impossible d’ouvrir {0} : {1} +Failed to start,Не удалось запустить,启动失败,啟動失敗,起動に失敗しました,시작 실패,Échec du démarrage +Failed to update ss:// protocol association,Не удалось обновить ассоциацию протокола ss://,无法更新 ss:// 协议关联,無法更新 ss:// 通訊協定關聯,ss:// プロトコルの関連付けを更新できませんでした,ss:// 프로토콜 연결을 업데이트하지 못했습니다,Échec de la mise à jour de l’association du protocole ss:// +Failed to update Start on Boot.,Не удалось изменить автозагрузку.,更新开机启动失败。,更新開機啟動失敗。,スタートアップ設定の更新に失敗しました。,시작 시 실행 설정을 업데이트하지 못했습니다.,Échec de la mise à jour du démarrage automatique. +Font family,Семейство шрифта,字体系列,字型系列,フォントファミリー,글꼴 패밀리,Famille de polices +Forward proxy settings saved.,Настройки внешнего прокси сохранены.,上游代理设置已保存。,上游 Proxy 設定已儲存。,上流プロキシ設定を保存しました。,업스트림 프록시 설정을 저장했습니다.,Les paramètres du proxy amont ont été enregistrés. +Game compatibility,Совместимость с играми,游戏兼容性,遊戲相容性,ゲーム互換性,게임 호환성,Compatibilité des jeux +Game Mode,Игровой режим,游戏模式,遊戲模式,ゲームモード,게임 모드,Mode jeu +Game Mode is automatic,Игровой режим включается автоматически,游戏模式是自动的,遊戲模式是自動的,Game Mode は自動です,Game Mode는 자동입니다,Le Game Mode est automatique +game.exe or C:\Games\Game\game.exe,game.exe или C:\Games\Game\game.exe,game.exe 或 C:\Games\Game\game.exe,game.exe 或 C:\Games\Game\game.exe,game.exe または C:\Games\Game\game.exe,game.exe 또는 C:\Games\Game\game.exe,game.exe ou C:\Games\Game\game.exe +GeoSite,GeoSite,GeoSite,GeoSite,GeoSite,GeoSite,GeoSite +GeoSite source list saved.,Список источников GeoSite сохранён.,GeoSite 源列表已保存。,GeoSite 來源清單已儲存。,GeoSite ソース一覧を保存しました。,GeoSite 소스 목록을 저장했습니다.,La liste des sources GeoSite a été enregistrée. +GeoSite update did not complete,Обновление GeoSite не завершено,GeoSite 更新未完成,GeoSite 更新未完成,GeoSite の更新が完了しませんでした,GeoSite 업데이트가 완료되지 않았습니다,La mise à jour GeoSite ne s’est pas terminée +Global shortcuts,Глобальные горячие клавиши,全局快捷键,全域快速鍵,グローバルショートカット,전역 단축키,Raccourcis globaux +Hotkeys,Горячие клавиши,编辑快捷键,編輯快速鍵,ホットキーの編集,단축키 수정,Modifier les raccourcis clavier +Hotkeys saved and registered.,Горячие клавиши сохранены и зарегистрированы.,快捷键已保存并注册。,快速鍵已儲存並註冊。,ホットキーを保存して登録しました。,단축키를 저장하고 등록했습니다.,Les raccourcis ont été enregistrés et inscrits. +How tray mode works,Как работает режим трея,托盘模式的工作方式,系統匣模式的運作方式,トレイモードの仕組み,트레이 모드 작동 방식,Fonctionnement du mode zone de notification +Import,Импортировать,导入,匯入,インポート,가져오기,Importer +Import server,Импорт сервера,导入服务器,匯入伺服器,サーバーをインポート,서버 가져오기,Importer un serveur +Import server configuration?,Импортировать конфигурацию сервера?,导入服务器配置?,匯入伺服器設定?,サーバー設定をインポートしますか?,서버 구성을 가져오시겠습니까?,Importer la configuration du serveur ? +In 0 B/s · Out 0 B/s,Вход 0 Б/с · Выход 0 Б/с,入 0 B/s · 出 0 B/s,入 0 B/s · 出 0 B/s,受信 0 B/s · 送信 0 B/s,수신 0 B/s · 송신 0 B/s,Entrée 0 B/s · Sortie 0 B/s +In {0}/s · Out {1}/s,Вход {0}/с · Выход {1}/с,入 {0}/s · 出 {1}/s,入 {0}/s · 出 {1}/s,受信 {0}/s · 送信 {1}/s,수신 {0}/s · 송신 {1}/s,Entrée {0}/s · Sortie {1}/s +In: 0 B · Out: 0 B,Вход: 0 Б · Выход: 0 Б,入:0 B · 出:0 B,入:0 B · 出:0 B,受信: 0 B · 送信: 0 B,수신: 0 B · 송신: 0 B,Entrée : 0 B · Sortie : 0 B +In: {0} · Out: {1},Вход: {0} · Выход: {1},入:{0} · 出:{1},入:{0} · 出:{1},受信: {0} · 送信: {1},수신: {0} · 송신: {1},Entrée : {0} · Sortie : {1} +Inactive,Неактивен,非活动,非啟用,非アクティブ,비활성,Inactif +Include prerelease versions,Учитывать предварительные версии,检查测试版更新,檢查發行前版本更新,ベータ版のアップデートも確認,개발 버전 업데이트 확인하기,Vérifier la version préliminaire +Info,Информация,信息,資訊,情報,정보,Informations +Invalid shortcut: {0},Некорректное сочетание клавиш: {0},无效快捷键:{0},無效的快速鍵:{0},無効なショートカット: {0},잘못된 단축키: {0},Raccourci invalide : {0} +Italic,Курсив,斜体,斜體,斜体,기울임꼴,Italique +Light,Светлая,浅色,淺色,ライト,밝게,Clair +"Live log viewer with traffic history, font, wrap, toolbar, and always-on-top settings.","Просмотр логов в реальном времени с историей трафика, настройками шрифта, переноса строк, панели инструментов и режима поверх окон.",实时日志查看器,包含流量历史、字体、自动换行、工具栏和置顶设置。,即時記錄檢視器,包含流量歷程、字型、自動換行、工具列與最上層設定。,トラフィック履歴、フォント、折り返し、ツールバー、常に手前の設定を備えたライブログビューアです。,"트래픽 기록, 글꼴, 줄 바꿈, 도구 모음 및 항상 위 설정이 있는 실시간 로그 뷰어입니다.","Visionneuse de journaux en direct avec historique du trafic, police, retour à la ligne, barre d’outils et option toujours au premier plan." +Local client,Локальный клиент,本地客户端,本機用戶端,ローカルクライアント,로컬 클라이언트,Client local +Local GeoSite,Локальный GeoSite,本地 GeoSite,本機 GeoSite,ローカル GeoSite,로컬 GeoSite,GeoSite local +Local only,Только локально,仅本地,僅本機,ローカルのみ,로컬 전용,Local uniquement +Local PAC updated from GeoSite,Локальный PAC обновлён из GeoSite,已从 GeoSite 更新本地 PAC,已從 GeoSite 更新本機 PAC,GeoSite からローカル PAC を更新しました,GeoSite에서 로컬 PAC를 업데이트했습니다,PAC local mis à jour depuis GeoSite +Local PAC URL copied,URL локального PAC скопирован,本地 PAC URL 已复制,本機 PAC URL 已複製,ローカル PAC URL をコピーしました,로컬 PAC URL을 복사했습니다,URL PAC locale copiée +Local proxy port changed to {0}.,Порт локального прокси изменён на {0}.,本地代理端口已更改为 {0}。,本機 Proxy 連接埠已變更為 {0}。,ローカルプロキシポートを {0} に変更しました。,로컬 프록시 포트를 {0}(으)로 변경했습니다.,Le port proxy local a été changé en {0}. +Local SOCKS/HTTP port,Локальный порт SOCKS/HTTP,本地 SOCKS/HTTP 端口,本機 SOCKS/HTTP 連接埠,ローカル SOCKS/HTTP ポート,로컬 SOCKS/HTTP 포트,Port SOCKS/HTTP local +Log file cleared.,Файл лога очищен.,日志文件已清除。,記錄檔已清除。,ログファイルを消去しました。,로그 파일을 지웠습니다.,Le fichier journal a été effacé. +Log file has not been created yet.,Файл лога ещё не создан.,尚未创建日志文件。,尚未建立記錄檔。,ログファイルはまだ作成されていません。,로그 파일이 아직 생성되지 않았습니다.,Le fichier journal n’a pas encore été créé. +Log font,Шрифт лога,日志字体,記錄字型,ログフォント,로그 글꼴,Police du journal +Logs,Логи,显示日志,顯示記錄檔,ログの表示,로그 보기,Afficher les journaux +Manage proxy servers and local client settings.,Управляйте прокси-серверами и настройками локального клиента.,管理代理服务器和本地客户端设置。,管理 Proxy 伺服器與本機用戶端設定。,プロキシサーバーとローカルクライアント設定を管理します。,프록시 서버 및 로컬 클라이언트 설정을 관리합니다.,Gérer les serveurs proxy et les paramètres du client local. +Manage SIP008 / online configuration sources.,Управляйте источниками SIP008 / онлайн-конфигураций.,管理 SIP008 / 在线配置源。,管理 SIP008 / 線上設定來源。,SIP008 / オンライン設定ソースを管理します。,SIP008 / 온라인 구성 소스를 관리합니다.,Gérer les sources de configuration SIP008 / en ligne. +Move down,Переместить вниз,下移,下移,下へ,아래로 이동,Descendre +Move up,Переместить вверх,上移,上移,上へ,위로 이동,Monter +Network service,Сетевая служба,网络服务,網路服務,ネットワークサービス,네트워크 서비스,Service réseau +New server {0},Новый сервер {0},新服务器 {0},新伺服器 {0},新しいサーバー {0},새 서버 {0},Nouveau serveur {0} +No application-specific rules.,Правила для отдельных приложений не настроены.,没有按应用程序规则。,沒有個別應用程式規則。,アプリケーション固有のルールはありません。,애플리케이션별 규칙이 없습니다.,Aucune règle spécifique à une application. +No elevation. Uses the normal local proxy path.,Без повышения прав. Используется обычный локальный прокси.,无需提升权限。使用普通本地代理路径。,不需要提高權限。使用一般本機 Proxy 路徑。,昇格は不要です。通常のローカルプロキシ経路を使用します。,권한 상승이 필요 없습니다. 일반 로컬 프록시 경로를 사용합니다.,Aucune élévation. Utilise le chemin proxy local normal. +No game applications are configured yet.,Приложения для игрового режима ещё не настроены.,尚未配置游戏模式应用程序。,尚未設定遊戲模式應用程式。,Game Mode のアプリケーションはまだ設定されていません。,Game Mode 애플리케이션이 아직 구성되지 않았습니다.,Aucune application de Game Mode n’est encore configurée. +No proxy,Без прокси,无代理,無 Proxy,プロキシなし,프록시 없음,Aucun proxy +No server configured,Сервер не настроен,未配置服务器,未設定伺服器,サーバー未設定,구성된 서버 없음,Aucun serveur configuré +No valid ss:// server links were found.,Корректные ссылки серверов ss:// не найдены.,未找到有效的 ss:// 服务器链接。,找不到有效的 ss:// 伺服器連結。,有効な ss:// サーバーリンクが見つかりませんでした。,유효한 ss:// 서버 링크를 찾지 못했습니다.,Aucun lien de serveur ss:// valide n’a été trouvé. +Not checked yet.,Проверка ещё не выполнялась.,尚未检查。,尚未檢查。,まだ確認していません。,아직 확인하지 않음.,Pas encore vérifié. +Not configured,Не настроен,未配置,未設定,未設定,구성되지 않음,Non configuré +Not now,Не сейчас,稍后,稍後,後で,나중에,Plus tard +Not running,Не запущено,未运行,未執行,未実行,실행 중 아님,Non exécuté +Off,Выкл.,关闭,關閉,オフ,끔,Désactivé +On,Вкл.,开启,開啟,オン,켬,Activé +One URL per line,Один URL на строку,每行一个 URL,每行一個 URL,1行に1つの URL,한 줄에 URL 하나,Une URL par ligne +Online,Онлайн,在线,線上,オンライン,온라인,En ligne +Online PAC URL,URL онлайн-PAC,在线 PAC URL,線上 PAC URL,オンライン PAC URL,온라인 PAC URL,URL PAC en ligne +Online PAC URL saved.,URL онлайн-PAC сохранён.,在线 PAC URL 已保存。,線上 PAC URL 已儲存。,オンライン PAC URL を保存しました。,온라인 PAC URL을 저장했습니다.,L’URL PAC en ligne a été enregistrée. +Open,Открыть,打开,開啟,開く,열기,Ouvrir +Open file,Открыть файл,打开文件,開啟檔案,ファイルを開く,파일 열기,Ouvrir le fichier +Open location,Открыть расположение,打开位置,開啟位置,場所を開く,위치 열기,Ouvrir l’emplacement +Overview,Обзор,概览,概覽,概要,개요,Vue d’ensemble +PAC / GeoSite,PAC / GeoSite,PAC / GeoSite,PAC / GeoSite,PAC / GeoSite,PAC / GeoSite,PAC / GeoSite +PAC mode,Режим PAC,PAC 模式,PAC 模式,PAC モード,PAC 모드,Mode PAC +Paste one or more ss:// links,Вставьте одну или несколько ссылок ss://,粘贴一个或多个 ss:// 链接,貼上一個或多個 ss:// 連結,1つ以上の ss:// リンクを貼り付け,하나 이상의 ss:// 링크 붙여넣기,Coller un ou plusieurs liens ss:// +Paused,Приостановлен,已暂停,已暫停,一時停止,일시 중지됨,En pause +Please input PAC URL before enabling Online PAC.,Перед включением онлайн-PAC укажите URL PAC.,启用在线 PAC 前请输入 PAC URL。,啟用線上 PAC 前請輸入 PAC URL。,オンライン PAC を有効にする前に PAC URL を入力してください。,온라인 PAC를 활성화하기 전에 PAC URL을 입력하세요.,Veuillez saisir l’URL PAC avant d’activer le PAC en ligne. +Please input PAC URL.,Укажите URL PAC.,请输入 PAC URL。,請輸入 PAC URL。,PAC URL を入力してください。,PAC URL을 입력하세요.,Veuillez saisir l’URL PAC. +Pre-release,Предварительная версия,预发布,預發行,プレリリース,사전 릴리스,Préversion +Project,Проект,项目,專案,プロジェクト,프로젝트,Projet +Proxy,Прокси,代理,代理,プロキシ,프록시,Proxy +Proxy address is required.,Необходимо указать адрес прокси.,必须填写代理地址。,必須填寫 Proxy 位址。,プロキシアドレスが必要です。,프록시 주소가 필요합니다.,L’adresse du proxy est requise. +Proxy port must be between 1 and 65535.,Порт прокси должен быть в диапазоне от 1 до 65535.,代理端口必须在 1 到 65535 之间。,Proxy 連接埠必須介於 1 到 65535 之間。,プロキシポートは 1~65535 の範囲で指定してください。,프록시 포트는 1에서 65535 사이여야 합니다.,Le port du proxy doit être compris entre 1 et 65535. +Refresh,Обновить,刷新,重新整理,更新,새로 고침,Actualiser +Regenerate Local PAC after application updates,Пересоздавать локальный PAC после обновлений приложения,应用更新后重新生成本地 PAC,應用程式更新後重新產生本機 PAC,アプリ更新後にローカル PAC を再生成,앱 업데이트 후 로컬 PAC 다시 생성,Régénérer le PAC local après les mises à jour de l’application +Register all,Зарегистрировать все,全部注册,全部註冊,すべて登録,모두 등록,Tout enregistrer +Register hotkeys at startup,Регистрировать горячие клавиши при запуске,启动时注册快捷键,啟動時註冊快速鍵,起動時にホットキーを登録,시작 시 단축키 등록,Enregistrer les raccourcis au démarrage +Release,Релиз,正式版本,正式版本,リリース,릴리스,Version +Release notes,Примечания к выпуску,发行说明,版本資訊,リリースノート,릴리스 정보,Notes de version +Release notes are unavailable.,Примечания к выпуску недоступны.,发行说明不可用。,版本資訊無法使用。,リリースノートを利用できません。,릴리스 정보를 사용할 수 없습니다.,Les notes de version ne sont pas disponibles. +Remove,Удалить,删除,移除,削除,제거,Supprimer +Report issue,Сообщить о проблеме,报告问题,回報問題,問題を報告,문제 보고,Signaler un problème +Require secret for local PAC URL,Требовать секрет для URL локального PAC,要求本地 PAC URL 使用密钥,要求本機 PAC URL 使用密鑰,ローカル PAC URL にシークレットを要求,로컬 PAC URL에 비밀 키 요구,Exiger un secret pour l’URL PAC locale +Restore default,Восстановить по умолчанию,恢复默认,還原預設值,既定値に戻す,기본값 복원,Restaurer les valeurs par défaut +"Rules are matched by executable name, full path, or wildcard. Proxy, Direct and Block are explicit actions.","Правила сопоставляются по имени исполняемого файла, полному пути или маске. Действия «Прокси», «Напрямую» и «Блокировать» задаются явно.",规则按可执行文件名、完整路径或通配符匹配。代理、直连和阻止是明确的操作。,規則會依執行檔名稱、完整路徑或萬用字元比對。Proxy、直接連線與封鎖是明確的動作。,ルールは実行ファイル名、完全パス、またはワイルドカードで照合します。Proxy、Direct、Block は明示的な操作です。,"규칙은 실행 파일 이름, 전체 경로 또는 와일드카드로 일치합니다. Proxy, Direct, Block은 명시적 동작입니다.","Les règles correspondent au nom de l’exécutable, au chemin complet ou à un motif générique. Proxy, Direct et Block sont des actions explicites." +Running,Работает,运行中,執行中,実行中,실행 중,En cours d’exécution +Runs quietly in the background,Работает в фоне,在后台安静运行,在背景安靜執行,バックグラウンドで静かに実行,백그라운드에서 조용히 실행,S’exécute discrètement en arrière-plan +Runtime mode,Фактический режим,运行模式,執行模式,実行モード,런타임 모드,Mode d’exécution +Save,Сохранить,保存,儲存,保存,저장,Enregistrer +Save PAC URL,Сохранить URL PAC,保存 PAC URL,儲存 PAC URL,PAC URL を保存,PAC URL 저장,Enregistrer l’URL PAC +Save routing,Сохранить маршрутизацию,保存路由,儲存路由,ルーティングを保存,라우팅 저장,Enregistrer le routage +Save sources,Сохранить источники,保存源,儲存來源,ソースを保存,소스 저장,Enregistrer les sources +"Saved, but Windows refused: {0}","Сохранено, но Windows отклонила: {0}",已保存,但 Windows 拒绝:{0},已儲存,但 Windows 拒絕:{0},保存しましたが、Windows に拒否されました: {0},저장했지만 Windows에서 거부했습니다: {0},"Enregistré, mais Windows a refusé : {0}" +Scale 1 KiB/s,Шкала 1 КиБ/с,刻度 1 KiB/s,刻度 1 KiB/s,スケール 1 KiB/s,스케일 1 KiB/s,Échelle 1 Kio/s +Scale {0}/s,Шкала {0}/с,刻度 {0}/s,刻度 {0}/s,スケール {0}/s,스케일 {0}/s,Échelle {0}/s +Secure,Защищённый,安全,安全,セキュア,보안,Sécurisé +Server configuration imported.,Конфигурация сервера импортирована.,服务器配置已导入。,伺服器設定已匯入。,サーバー設定をインポートしました。,서버 구성을 가져왔습니다.,La configuration du serveur a été importée. +Server configuration saved.,Конфигурация сервера сохранена.,服务器配置已保存。,伺服器設定已儲存。,サーバー設定を保存しました。,서버 구성을 저장했습니다.,La configuration du serveur a été enregistrée. +Server is not configured.,Сервер не настроен.,服务器未配置。,伺服器未設定。,サーバーは設定されていません。,서버가 구성되지 않았습니다.,Le serveur n’est pas configuré. +Server link copied to the clipboard.,Ссылка на сервер скопирована в буфер обмена.,服务器链接已复制到剪贴板。,伺服器連結已複製到剪貼簿。,サーバーリンクをクリップボードにコピーしました。,서버 링크를 클립보드에 복사했습니다.,Le lien du serveur a été copié dans le Presse-papiers. +Server {0},Сервер {0},服务器 {0},伺服器 {0},サーバー {0},서버 {0},Serveur {0} +Settings,Настройки,设置,設定,設定,설정,Paramètres +Shadowsocks Reborn,Shadowsocks Reborn,Shadowsocks Reborn,Shadowsocks Reborn,Shadowsocks Reborn,Shadowsocks Reborn,Shadowsocks Reborn +Share,Поделиться,分享,分享,共有,공유,Partager +Share / QR,Поделиться / QR,分享 / QR,分享 / QR,共有 / QR,공유 / QR,Partage / QR +"Share a server using the same list + QR workflow as the original client, or import ss:// links.","Поделитесь сервером через список и QR-код, как в оригинальном клиенте, или импортируйте ссылки ss://.",使用与原客户端相同的列表 + QR 流程分享服务器,或导入 ss:// 链接。,使用與原始用戶端相同的清單 + QR 流程分享伺服器,或匯入 ss:// 連結。,元のクライアントと同じ一覧 + QR の流れでサーバーを共有するか、ss:// リンクをインポートします。,기존 클라이언트와 동일한 목록 + QR 흐름으로 서버를 공유하거나 ss:// 링크를 가져옵니다.,"Partager un serveur avec le même flux liste + QR que le client d’origine, ou importer des liens ss://." +Shared,Доступ из сети,已共享,已分享,共有済み,공유됨,Partagé +Shell initialization warning: {0},Предупреждение инициализации оболочки: {0},Shell 初始化警告:{0},Shell 初始化警告:{0},シェル初期化の警告: {0},셸 초기화 경고: {0},Avertissement d’initialisation du shell : {0} +Shortcut format,Формат сочетания клавиш,快捷键格式,快速鍵格式,ショートカット形式,단축키 형식,Format du raccourci +Single instance · tray active · controller unavailable,Один экземпляр · трей активен · контроллер недоступен,单实例 · 托盘活动 · 控制器不可用,單一執行個體 · 系統匣作用中 · 控制器無法使用,単一インスタンス · トレイ有効 · コントローラー利用不可,단일 인스턴스 · 트레이 활성 · 컨트롤러 사용 불가,Instance unique · zone de notification active · contrôleur indisponible +Single instance · tray active · system shell ready,Один экземпляр · трей активен · системная оболочка готова,单实例 · 托盘活动 · 系统 shell 就绪,單一執行個體 · 系統匣作用中 · 系統 Shell 就緒,単一インスタンス · トレイ有効 · システムシェル準備完了,단일 인스턴스 · 트레이 활성 · 시스템 셸 준비됨,Instance unique · zone de notification active · shell système prêt +Size (pt),Размер (пт),大小(pt),大小(pt),サイズ (pt),크기(pt),Taille (pt) +Skip this version,Пропустить эту версию,跳过此版本,略過此版本,このバージョンをスキップ,이 버전 건너뛰기,Ignorer cette version +Source URL copied.,URL источника скопирован.,源 URL 已复制。,來源 URL 已複製。,ソース URL をコピーしました。,소스 URL을 복사했습니다.,URL source copiée. +Sources are tried independently and merged. A missing .sha256sum sidecar is non-fatal.,"Источники обрабатываются независимо, а результаты объединяются. Отсутствие файла .sha256sum не является критической ошибкой.",各源会独立尝试并合并。缺少 .sha256sum sidecar 不会导致失败。,各來源會獨立嘗試並合併。缺少 .sha256sum sidecar 不會造成失敗。,各ソースは個別に試行され、結果がマージされます。.sha256sum サイドカーがなくても致命的ではありません。,각 소스는 독립적으로 시도되고 병합됩니다. .sha256sum 사이드카가 없어도 치명적이지 않습니다.,Les sources sont essayées indépendamment puis fusionnées. L’absence d’un fichier annexe .sha256sum n’est pas bloquante. +ss:// link,Ссылка ss://,ss:// 链接,ss:// 連結,ss:// リンク,ss:// 링크,Lien ss:// +Start on Boot disabled,Автозагрузка отключена,开机启动已禁用,開機啟動已停用,スタートアップを無効化しました,시작 시 실행 비활성화됨,Démarrage automatique désactivé +Start on Boot enabled,Автозагрузка включена,开机启动已启用,開機啟動已啟用,スタートアップを有効化しました,시작 시 실행 활성화됨,Démarrage automatique activé +Starting…,Запуск…,正在启动…,正在啟動…,起動中…,시작하는 중…,Démarrage… +Startup,Автозапуск,启动,啟動,スタートアップ,시작,Démarrage +Stay,Остаться,保留,保留,このまま,유지,Conserver +Stopped,Остановлен,已停止,已停止,停止,중지됨,Arrêté +Suspended,Приостановлен,已暂停,已暫停,一時停止,일시 중지됨,Suspendu +Suspended by Game Mode,Приостановлен игровым режимом,被游戏模式暂停,由遊戲模式暫停,Game Mode により一時停止,Game Mode에 의해 일시 중지됨,Suspendu par le Game Mode +System,Системная,系统,系統,システム,시스템,Système +System shell,Системная оболочка,系统 shell,系統 Shell,システムシェル,시스템 셸,Shell système +TCP capture,Захват TCP,TCP 捕获,TCP 擷取,TCP キャプチャ,TCP 캡처,Capture TCP +TCP redirect port,Порт перенаправления TCP,TCP 重定向端口,TCP 重新導向連接埠,TCP リダイレクトポート,TCP 리디렉션 포트,Port de redirection TCP +The application list could not be applied.,Не удалось применить список приложений.,无法应用应用程序列表。,無法套用應用程式清單。,アプリケーション一覧を適用できませんでした。,애플리케이션 목록을 적용할 수 없습니다.,Impossible d’appliquer la liste des applications. +Theme follows Windows by default. Choose a fixed theme only when you explicitly want to override the system setting.,По умолчанию тема следует настройкам Windows. Выберите фиксированную тему только если хотите переопределить системную настройку.,默认跟随 Windows 主题。仅在明确需要覆盖系统设置时选择固定主题。,預設跟隨 Windows 主題。只有在明確需要覆寫系統設定時才選擇固定主題。,既定では Windows テーマに従います。システム設定を明示的に上書きしたい場合のみ固定テーマを選択してください。,기본적으로 Windows 테마를 따릅니다. 시스템 설정을 명시적으로 재정의하려는 경우에만 고정 테마를 선택하세요.,Le thème suit Windows par défaut. Choisissez un thème fixe uniquement si vous souhaitez explicitement remplacer le réglage système. +Theme now follows Windows.,Теперь тема следует настройкам Windows.,主题现在跟随 Windows。,主題現在跟隨 Windows。,テーマは Windows に従うようになりました。,이제 테마가 Windows를 따릅니다.,Le thème suit désormais Windows. +This server has not been configured. Discard it and switch to the selected server?,Этот сервер не настроен. Отбросить его и перейти к выбранному серверу?,此服务器尚未配置。是否丢弃它并切换到选中的服务器?,此伺服器尚未設定。是否捨棄它並切換至選取的伺服器?,このサーバーは未設定です。破棄して選択したサーバーに切り替えますか?,이 서버는 구성되지 않았습니다. 버리고 선택한 서버로 전환하시겠습니까?,Ce serveur n’est pas configuré. L’ignorer et passer au serveur sélectionné ? +This version will be skipped.,Эта версия будет пропущена.,将跳过此版本。,將略過此版本。,このバージョンはスキップされます。,이 버전은 건너뜁니다.,Cette version sera ignorée. +Timeout must be between 1 and {0} seconds.,Тайм-аут должен быть от 1 до {0} секунд.,超时必须在 1 到 {0} 秒之间。,逾時必須介於 1 到 {0} 秒之間。,タイムアウトは 1~{0} 秒の範囲で指定してください。,제한 시간은 1초에서 {0}초 사이여야 합니다.,Le délai doit être compris entre 1 et {0} secondes. +Top most,Поверх остальных окон,始终置顶,最上層,常に手前に表示,항상 위,Toujours au premier plan +Traffic,Трафик,流量,流量,トラフィック,트래픽,Trafic +Traffic unavailable,Данные о трафике недоступны,流量不可用,流量無法使用,トラフィック利用不可,트래픽 사용 불가,Trafic indisponible +Traffic · last 60 seconds,Трафик · последние 60 секунд,流量 · 最近 60 秒,流量 · 最近 60 秒,トラフィック · 過去60秒,트래픽 · 최근 60초,Trafic · 60 dernières secondes +Transparent TCP/UDP capture through the elevated NetworkService helper.,"Прозрачный захват TCP/UDP через NetworkService, запущенный с повышенными правами.",通过已提升权限的 NetworkService helper 进行透明 TCP/UDP 捕获。,透過已提高權限的 NetworkService helper 進行透明 TCP/UDP 擷取。,昇格された NetworkService ヘルパーによる透過 TCP/UDP キャプチャです。,권한이 상승된 NetworkService 도우미를 통한 투명 TCP/UDP 캡처입니다.,Capture TCP/UDP transparente via le helper NetworkService élevé. +Tray active · {0} hotkey(s) failed to register,Трей активен · не удалось зарегистрировать горячих клавиш: {0},托盘活动 · {0} 个快捷键注册失败,系統匣作用中 · {0} 個快速鍵註冊失敗,トレイ有効 · {0} 個のホットキー登録に失敗,트레이 활성 · 단축키 {0}개 등록 실패,Zone de notification active · échec d’enregistrement de {0} raccourci(s) +Tray command failed: {0},Ошибка команды трея: {0},托盘命令失败:{0},系統匣命令失敗:{0},トレイコマンドに失敗しました: {0},트레이 명령 실패: {0},Échec de la commande de la zone de notification : {0} +Triggered by,Активирован приложением,触发者,觸發者,トリガー,트리거,Déclenché par +Type,Тип,类型,類型,種類,유형,Type +UDP capture,Захват UDP,UDP 捕获,UDP 擷取,UDP キャプチャ,UDP 캡처,Capture UDP +UDP redirect port,Порт перенаправления UDP,UDP 重定向端口,UDP 重新導向連接埠,UDP リダイレクトポート,UDP 리디렉션 포트,Port de redirection UDP +Unable to read log: {0},Не удалось прочитать лог: {0},无法读取日志:{0},無法讀取記錄:{0},ログを読み取れません: {0},로그를 읽을 수 없습니다: {0},Impossible de lire le journal : {0} +Unavailable,Недоступно,不可用,無法使用,利用不可,사용 불가,Indisponible +Unconfigured server,Ненастроенный сервер,未配置服务器,未設定伺服器,未設定サーバー,구성되지 않은 서버,Serveur non configuré +Update,Обновить,更新,更新,更新,업데이트,Mise à jour +Update all,Обновить всё,全部更新,全部更新,すべて更新,모두 업데이트,Tout mettre à jour +Update available: {0},Доступно обновление: {0},有可用更新:{0},有可用更新:{0},利用可能な更新: {0},사용 가능한 업데이트: {0},Mise à jour disponible : {0} +Update postponed for this session.,Обновление отложено до следующего запуска.,本次会话已推迟更新。,本次工作階段已延後更新。,このセッションでは更新を延期しました。,이번 세션에서 업데이트를 연기했습니다.,La mise à jour est reportée pour cette session. +"Use an executable name, full path, or wildcard pattern. Game Mode is automatic and is never a selectable traffic mode.","Укажите имя исполняемого файла, полный путь или маску. Игровой режим включается автоматически и никогда не является выбираемым режимом трафика.",使用可执行文件名、完整路径或通配符模式。游戏模式是自动的,永远不是可选择的流量模式。,使用執行檔名稱、完整路徑或萬用字元模式。遊戲模式是自動的,永遠不是可選擇的流量模式。,実行ファイル名、完全パス、またはワイルドカードパターンを使用します。Game Mode は自動であり、選択可能なトラフィックモードではありません。,"실행 파일 이름, 전체 경로 또는 와일드카드 패턴을 사용합니다. Game Mode는 자동이며 선택 가능한 트래픽 모드가 아닙니다.","Utiliser un nom d’exécutable, un chemin complet ou un motif générique. Le Game Mode est automatique et n’est jamais un mode de trafic sélectionnable." +Use Shadowsocks as the Windows system proxy,Использовать Shadowsocks как системный прокси Windows,将 Shadowsocks 用作 Windows 系统代理,將 Shadowsocks 用作 Windows 系統 Proxy,Shadowsocks を Windows システムプロキシとして使用,Shadowsocks를 Windows 시스템 프록시로 사용,Utiliser Shadowsocks comme proxy système Windows +Username and password must be specified together.,Имя пользователя и пароль должны быть указаны вместе.,必须同时指定用户名和密码。,必須同時指定使用者名稱與密碼。,ユーザー名とパスワードは両方指定する必要があります。,사용자 이름과 비밀번호를 함께 지정해야 합니다.,Le nom d’utilisateur et le mot de passe doivent être spécifiés ensemble. +"Version information, project links and update controls.","Информация о версии, ссылки проекта и управление обновлениями.",版本信息、项目链接和更新控制。,版本資訊、專案連結與更新控制項。,バージョン情報、プロジェクトリンク、更新コントロールです。,"버전 정보, 프로젝트 링크 및 업데이트 제어입니다.","Informations de version, liens du projet et contrôles de mise à jour." +WinDivert,WinDivert,WinDivert,WinDivert,WinDivert,WinDivert,WinDivert +Windows proxy,Прокси Windows,Windows 代理,Windows Proxy,Windows プロキシ,Windows 프록시,Proxy Windows +Windows refused: {0},Windows отклонила: {0},Windows 拒绝:{0},Windows 拒絕:{0},Windows に拒否されました: {0},Windows에서 거부됨: {0},Windows a refusé : {0} +Wrap text,Переносить строки,自动换行,自動換行,テキストを折り返す,텍스트 줄 바꿈,Renvoyer à la ligne +You are up to date · {0},Установлена актуальная версия · {0},已是最新版本 · {0},已是最新版本 · {0},最新です · {0},최신 버전입니다 · {0},Vous êtes à jour · {0} +{0} available: {1},{0} доступен: {1},{0} 可用:{1},{0} 可用:{1},{0} が利用可能: {1},{0} 사용 가능: {1},{0} disponible : {1} +{0} Fix the current server before selecting another one.,{0} Исправьте текущий сервер перед выбором другого.,{0} 请先修复当前服务器,再选择其他服务器。,{0} 請先修正目前伺服器,再選擇其他伺服器。,{0} 別のサーバーを選択する前に現在のサーバーを修正してください。,{0} 다른 서버를 선택하기 전에 현재 서버를 수정하세요.,{0} Corrigez le serveur actuel avant d’en sélectionner un autre. +{0} theme enabled.,Включена тема «{0}».,已启用 {0} 主题。,已啟用 {0} 主題。,{0} テーマを有効にしました。,{0} 테마를 활성화했습니다.,Thème {0} activé. +{0} · {1} mode,{0} · режим {1},{0} · {1} 模式,{0} · {1} 模式,{0} · {1} モード,{0} · {1} 모드,{0} · mode {1} +… showing the last 256 KiB …,… показаны последние 256 КиБ …,…仅显示最后 256 KiB…,…僅顯示最後 256 KiB…,…末尾 256 KiB のみ表示…,…마지막 256 KiB만 표시…,… affichage des 256 derniers Kio … +Version {0},Версия {0},版本 {0},版本 {0},バージョン {0},버전 {0},Version {0} +A configured game temporarily changes the runtime state to Game and suspends WinDivert. It is not a third selectable capture mode.,Настроенная игра временно переводит runtime-состояние в Game и приостанавливает WinDivert. Это не третий выбираемый режим захвата.,已配置的游戏会暂时将运行状态切换为 Game 并暂停 WinDivert。它不是第三种可选的捕获模式。,已設定的遊戲會暫時將執行狀態切換為 Game 並暫停 WinDivert。它不是第三種可選的擷取模式。,設定済みのゲームが実行されると、実行状態が一時的に Game に切り替わり WinDivert が停止します。これは3つ目の選択可能なキャプチャモードではありません。,설정된 게임이 실행되면 런타임 상태가 일시적으로 Game으로 전환되고 WinDivert가 중지됩니다. 이는 세 번째로 선택 가능한 캡처 모드가 아닙니다.,Un jeu configuré fait temporairement passer l’état d’exécution à Game et suspend WinDivert. Il ne s’agit pas d’un troisième mode de capture sélectionnable. +Focus a shortcut field and press Ctrl/Alt/Shift/Win plus a key. Press Delete or Backspace with a modifier to use that key; use Esc to clear the field. The status column shows the last registration result.,Выберите поле сочетания клавиш и нажмите Ctrl/Alt/Shift/Win вместе с нужной клавишей. Delete или Backspace с модификатором можно использовать как клавишу сочетания; Esc очищает поле. В колонке состояния показывается результат последней регистрации.,聚焦快捷键字段,然后按 Ctrl/Alt/Shift/Win 加一个按键。按住修饰键时可使用 Delete 或 Backspace;按 Esc 清空字段。状态列显示上次注册结果。,聚焦快速鍵欄位,然後按 Ctrl/Alt/Shift/Win 加一個按鍵。按住修飾鍵時可使用 Delete 或 Backspace;按 Esc 清空欄位。狀態欄會顯示上次註冊結果。,ショートカット欄にフォーカスし、Ctrl/Alt/Shift/Win とキーを押します。修飾キーと一緒なら Delete または Backspace も使用できます。Esc で欄をクリアします。状態列には前回の登録結果が表示されます。,단축키 필드에 포커스를 두고 Ctrl/Alt/Shift/Win과 키를 누르세요. 수정 키와 함께 Delete 또는 Backspace도 사용할 수 있습니다. Esc로 필드를 지웁니다. 상태 열에는 마지막 등록 결과가 표시됩니다.,"Placez le focus dans un champ de raccourci puis appuyez sur Ctrl/Alt/Shift/Win avec une touche. Avec un modificateur, Delete ou Backspace peuvent être utilisés ; Esc efface le champ. La colonne d’état affiche le dernier résultat d’enregistrement." +Use the tray icon to reopen Shadowsocks Reborn. A plain second launch points you back to the tray instead of starting another proxy.,"Используйте значок в трее, чтобы снова открыть Shadowsocks Reborn. Обычный повторный запуск укажет на уже работающий экземпляр в трее вместо запуска ещё одного прокси.",使用托盘图标重新打开 Shadowsocks Reborn。普通的第二次启动只会指向已运行的托盘实例,不会启动另一个代理。,使用系統匣圖示重新開啟 Shadowsocks Reborn。一般的第二次啟動只會指向已執行的系統匣執行個體,不會啟動另一個 Proxy。,トレイアイコンから Shadowsocks Reborn を開き直してください。通常の2回目の起動は別のプロキシを開始せず、既存のトレイインスタンスへ誘導します。,트레이 아이콘을 사용해 Shadowsocks Reborn을 다시 여세요. 일반적인 두 번째 실행은 다른 프록시를 시작하지 않고 실행 중인 트레이 인스턴스로 안내합니다.,Utilisez l’icône de la zone de notification pour rouvrir Shadowsocks Reborn. Un second lancement normal vous renvoie vers l’instance déjà active au lieu de démarrer un autre proxy. +GeoSite data refreshed and Local PAC was regenerated.,"Данные GeoSite обновлены, локальный PAC пересоздан.",GeoSite 数据已刷新,并重新生成了本地 PAC。,GeoSite 資料已重新整理,並重新產生本機 PAC。,GeoSite データを更新し、ローカル PAC を再生成しました。,GeoSite 데이터를 새로 고치고 로컬 PAC를 다시 생성했습니다.,Les données GeoSite ont été actualisées et le PAC local a été régénéré. +GeoSite refresh did not complete successfully.,Обновление GeoSite не завершилось успешно.,GeoSite 刷新未成功完成。,GeoSite 重新整理未成功完成。,GeoSite の更新が正常に完了しませんでした。,GeoSite 새로 고침이 정상적으로 완료되지 않았습니다.,L’actualisation GeoSite ne s’est pas terminée correctement. +Invalid port,Недопустимый порт,无效端口,無效的連接埠,無効なポート,잘못된 포트,Port invalide +Local proxy,Локальный прокси,本地代理,本機 Proxy,ローカルプロキシ,로컬 프록시,Proxy local +Online configuration update failed.,Не удалось обновить онлайн-конфигурацию.,在线配置更新失败。,線上設定更新失敗。,オンライン設定の更新に失敗しました。,온라인 구성 업데이트에 실패했습니다.,Échec de la mise à jour de la configuration en ligne. +Online configuration updated.,Онлайн-конфигурация обновлена.,在线配置已更新。,線上設定已更新。,オンライン設定を更新しました。,온라인 구성을 업데이트했습니다.,La configuration en ligne a été mise à jour. +QR code,QR-код,二维码,QR 碼,QR コード,QR 코드,Code QR +Server configuration,Конфигурация сервера,服务器配置,伺服器設定,サーバー設定,서버 구성,Configuration du serveur +The requested routing configuration could not be applied.,Не удалось применить запрошенную конфигурацию маршрутизации.,无法应用请求的路由配置。,無法套用要求的路由設定。,要求されたルーティング設定を適用できませんでした。,요청한 라우팅 구성을 적용할 수 없습니다.,Impossible d’appliquer la configuration de routage demandée. +Traffic routing applied.,Маршрутизация трафика применена.,流量路由已应用。,流量路由已套用。,トラフィックルーティングを適用しました。,트래픽 라우팅을 적용했습니다.,Le routage du trafic a été appliqué. +Import from URL:,Импортировать из URL:,从 URL 导入:,從 URL 匯入:,URL からインポート:,URL에서 가져오기:,Importer depuis l’URL : +Administrator mode could not be enabled.,Не удалось включить режим администратора.,无法启用管理员模式。,無法啟用系統管理員模式。,管理者モードを有効にできませんでした。,관리자 모드를 활성화할 수 없습니다.,Impossible d’activer le mode administrateur. +User mode could not be enabled.,Не удалось включить пользовательский режим.,无法启用用户模式。,無法啟用使用者模式。,ユーザーモードを有効にできませんでした。,사용자 모드를 활성화할 수 없습니다.,Impossible d’activer le mode utilisateur. +Detected games,Найденные игры,检测到的游戏,偵測到的遊戲,検出されたゲーム,감지된 게임,Jeux détectés +"Shadowsocks can scan local Steam, Epic Games, GOG and Xbox game libraries. Nothing is added until you choose Add.","Shadowsocks может сканировать локальные библиотеки Steam, Epic Games, GOG и Xbox. Ничего не добавляется, пока вы не нажмёте «Добавить».",Shadowsocks 可以扫描本地 Steam、Epic Games、GOG 和 Xbox 游戏库。只有选择“添加”后才会加入列表。,Shadowsocks 可以掃描本機 Steam、Epic Games、GOG 與 Xbox 遊戲庫。只有選擇「新增」後才會加入清單。,Shadowsocks はローカルの Steam、Epic Games、GOG、Xbox ライブラリをスキャンできます。「追加」を選ぶまで何も追加されません。,"Shadowsocks는 로컬 Steam, Epic Games, GOG 및 Xbox 게임 라이브러리를 검색할 수 있습니다. 추가를 선택하기 전에는 아무것도 추가되지 않습니다.","Shadowsocks peut analyser les bibliothèques locales Steam, Epic Games, GOG et Xbox. Rien n’est ajouté tant que vous ne choisissez pas Ajouter." +Scan installed games,Найти установленные игры,扫描已安装游戏,掃描已安裝遊戲,インストール済みゲームをスキャン,설치된 게임 검색,Analyser les jeux installés +Scan has not run yet.,Сканирование ещё не запускалось.,尚未运行扫描。,尚未執行掃描。,まだスキャンしていません。,아직 검색하지 않았습니다.,L’analyse n’a pas encore été exécutée. +Scanning installed games…,Поиск установленных игр…,正在扫描已安装游戏…,正在掃描已安裝遊戲…,インストール済みゲームをスキャンしています…,설치된 게임을 검색하는 중…,Analyse des jeux installés… +No supported game libraries were detected.,Поддерживаемые библиотеки игр не обнаружены.,未检测到受支持的游戏库。,未偵測到受支援的遊戲庫。,対応するゲームライブラリが検出されませんでした。,지원되는 게임 라이브러리가 감지되지 않았습니다.,Aucune bibliothèque de jeux prise en charge n’a été détectée. +Found {0} games.,Найдено игр: {0}.,找到 {0} 个游戏。,找到 {0} 個遊戲。,{0} 件のゲームが見つかりました。,게임 {0}개를 찾았습니다.,{0} jeu(x) trouvé(s). +Game scan failed.,Не удалось выполнить поиск игр.,游戏扫描失败。,遊戲掃描失敗。,ゲームのスキャンに失敗しました。,게임 검색에 실패했습니다.,Échec de l’analyse des jeux. +No detected games to show.,Найденных игр пока нет.,没有可显示的检测到游戏。,沒有可顯示的偵測遊戲。,表示できる検出済みゲームはありません。,표시할 감지된 게임이 없습니다.,Aucun jeu détecté à afficher. +Added,Добавлено,已添加,已新增,追加済み,추가됨,Ajouté +Add application manually,Добавить приложение вручную,手动添加应用程序,手動新增應用程式,アプリケーションを手動で追加,애플리케이션 수동 추가,Ajouter une application manuellement +"Live log viewer with traffic history, wrap, toolbar, and always-on-top settings.","Просмотр логов в реальном времени с историей трафика, переносом строк, панелью инструментов и режимом поверх окон.",带流量历史、自动换行、工具栏和置顶设置的实时日志查看器。,具有流量歷史、自動換行、工具列與最上層設定的即時記錄檢視器。,トラフィック履歴、折り返し、ツールバー、常に手前の設定を備えたライブログビューアです。,"트래픽 기록, 줄 바꿈, 도구 모음 및 항상 위 설정이 있는 실시간 로그 뷰어입니다.","Visionneuse de journaux en direct avec historique du trafic, retour à la ligne, barre d’outils et option toujours au premier plan." +Storage,Хранилище,存储,儲存,ストレージ,저장소,Stockage +Clean Mode is active. All writable data is stored in a temporary session directory and removed when Shadowsocks exits.,Clean Mode активен. Все изменяемые данные хранятся во временном каталоге сеанса и удаляются при выходе из Shadowsocks.,Clean Mode 已启用。所有可写数据都存储在临时会话目录中,并在 Shadowsocks 退出时删除。,Clean Mode 已啟用。所有可寫入資料都儲存在暫時工作階段目錄中,並在 Shadowsocks 結束時刪除。,Clean Mode が有効です。書き込み可能なデータはすべて一時セッション ディレクトリに保存され、Shadowsocks 終了時に削除されます。,Clean Mode가 활성화되었습니다. 쓰기 가능한 모든 데이터는 임시 세션 디렉터리에 저장되며 Shadowsocks 종료 시 삭제됩니다.,Clean Mode est actif. Toutes les données modifiables sont stockées dans un dossier de session temporaire et supprimées à la fermeture de Shadowsocks. +Start on Boot is unavailable in Clean Mode.,Автозагрузка недоступна в Clean Mode.,Clean Mode 下无法启用 Windows 启动项。,Clean Mode 下無法啟用 Windows 啟動項。,Clean Mode では Windows と同時に起動できません。,Clean Mode에서는 Windows 시작 프로그램을 사용할 수 없습니다.,Le démarrage avec Windows est indisponible en Clean Mode. +Failed to open data folder.,Не удалось открыть папку данных.,无法打开数据文件夹。,無法開啟資料資料夾。,データ フォルダーを開けませんでした。,데이터 폴더를 열 수 없습니다.,Impossible d’ouvrir le dossier de données. +All writable application data is stored under %LOCALAPPDATA%\Shadowsocks.,Все изменяемые данные приложения хранятся в %LOCALAPPDATA%\Shadowsocks.,所有可写应用数据都存储在 %LOCALAPPDATA%\Shadowsocks 中。,所有可寫入應用程式資料都儲存在 %LOCALAPPDATA%\Shadowsocks 中。,書き込み可能なアプリケーション データはすべて %LOCALAPPDATA%\Shadowsocks に保存されます。,쓰기 가능한 모든 애플리케이션 데이터는 %LOCALAPPDATA%\Shadowsocks에 저장됩니다.,Toutes les données modifiables de l’application sont stockées dans %LOCALAPPDATA%\Shadowsocks. +Open %LOCALAPPDATA%\Shadowsocks,Открыть %LOCALAPPDATA%\Shadowsocks,打开 %LOCALAPPDATA%\Shadowsocks,開啟 %LOCALAPPDATA%\Shadowsocks,%LOCALAPPDATA%\Shadowsocks を開く,%LOCALAPPDATA%\Shadowsocks 열기,Ouvrir %LOCALAPPDATA%\Shadowsocks +Open Clean Mode folder,Открыть папку Clean Mode,打开 Clean Mode 文件夹,開啟 Clean Mode 資料夾,Clean Mode フォルダーを開く,Clean Mode 폴더 열기,Ouvrir le dossier Clean Mode +Server Name,Имя сервера,服务器名称,伺服器名稱,サーバー名,서버 이름,Nom du serveur +Optional display name,Необязательное отображаемое имя,可选显示名称,選用顯示名稱,任意の表示名,선택적 표시 이름,Nom d’affichage facultatif +Delete server,Удалить сервер,删除服务器,刪除伺服器,サーバーを削除,서버 삭제,Supprimer le serveur +Server deleted.,Сервер удалён.,服务器已删除。,伺服器已刪除。,サーバーを削除しました。,서버가 삭제되었습니다.,Serveur supprimé. +Paste URL from clipboard,Вставить URL из буфера обмена,从剪贴板粘贴 URL,從剪貼簿貼上 URL,クリップボードから URL を貼り付け,클립보드에서 URL 붙여넣기,Coller l’URL depuis le presse-papiers +Open image,Открыть изображение,打开图片,開啟圖片,画像を開く,이미지 열기,Ouvrir une image +Scan QR from clipboard image,Сканировать QR из изображения в буфере,扫描剪贴板图片中的二维码,掃描剪貼簿圖片中的 QR 碼,クリップボード画像から QR をスキャン,클립보드 이미지에서 QR 스캔,Scanner le QR depuis l’image du presse-papiers +Clipboard does not contain an image.,В буфере обмена нет изображения.,剪贴板中没有图片。,剪貼簿中沒有圖片。,クリップボードに画像がありません。,클립보드에 이미지가 없습니다.,Le presse-papiers ne contient pas d’image. +Allow other devices to connect,Разрешить подключения других устройств,允许其他设备连入,允許其他裝置連入,他のデバイスからの接続を許可する,다른 기기에서 연결 허용,Autoriser d'autres appareils à se connecter +Add a per-application routing rule.,Добавить правило маршрутизации для отдельного приложения.,添加按应用程序路由规则。,新增個別應用程式路由規則。,アプリケーションごとのルーティングルールを追加します。,애플리케이션별 라우팅 규칙을 추가합니다.,Ajouter une règle de routage par application. +Add the entered URL to the online configuration source list.,Добавить введённый URL в список источников онлайн-конфигурации.,将输入的 URL 添加到在线配置源列表。,將輸入的 URL 新增至線上設定來源清單。,入力した URL をオンライン設定ソース一覧に追加します。,입력한 URL을 온라인 구성 소스 목록에 추가합니다.,Ajouter l’URL saisie à la liste des sources de configuration en ligne. +Add the manually entered application pattern to the Game Mode list.,Добавить введённое вручную приложение или шаблон в список игрового режима.,将手动输入的应用程序模式添加到游戏模式列表。,將手動輸入的應用程式模式新增至遊戲模式清單。,手動入力したアプリケーションパターンを Game Mode の一覧に追加します。,수동으로 입력한 애플리케이션 패턴을 Game Mode 목록에 추가합니다.,Ajouter le modèle d’application saisi manuellement à la liste du Game Mode. +Add this detected game executable to the automatic Game Mode list.,Добавить исполняемый файл найденной игры в список автоматического игрового режима.,将检测到的游戏可执行文件添加到自动游戏模式列表。,將偵測到的遊戲執行檔新增至自動遊戲模式清單。,検出されたゲーム実行ファイルを自動 Game Mode 一覧に追加します。,감지된 게임 실행 파일을 자동 Game Mode 목록에 추가합니다.,Ajouter cet exécutable de jeu détecté à la liste automatique du Game Mode. +Additional command-line arguments passed directly to the plugin process.,"Дополнительные аргументы командной строки, передаваемые непосредственно процессу плагина.",直接传递给插件进程的额外命令行参数。,直接傳遞給外掛程式處理程序的額外命令列參數。,プラグインプロセスに直接渡す追加のコマンドライン引数です。,플러그인 프로세스에 직접 전달되는 추가 명령줄 인수입니다.,Arguments de ligne de commande supplémentaires transmis directement au processus du plugin. +Allow devices on the local network to connect to the local Shadowsocks proxy port.,Разрешить устройствам в локальной сети подключаться к локальному прокси-порту Shadowsocks.,允许本地网络中的设备连接到本机 Shadowsocks 代理端口。,允許區域網路中的裝置連線至本機 Shadowsocks Proxy 連接埠。,ローカルネットワーク上のデバイスから、この Shadowsocks のローカルプロキシポートへの接続を許可します。,로컬 네트워크의 장치가 로컬 Shadowsocks 프록시 포트에 연결하도록 허용합니다.,Autoriser les appareils du réseau local à se connecter au port proxy local de Shadowsocks. +Append the entered URLs to the GeoSite source list.,Добавить введённые URL в список источников GeoSite.,将输入的 URL 追加到 GeoSite 源列表。,將輸入的 URL 附加至 GeoSite 來源清單。,入力した URL を GeoSite ソース一覧の末尾に追加します。,입력한 URL을 GeoSite 소스 목록에 추가합니다.,Ajouter les URL saisies à la liste des sources GeoSite. +Apply the local proxy port immediately.,Немедленно применить локальный порт прокси.,立即应用本地代理端口。,立即套用本機 Proxy 連接埠。,ローカルプロキシポートをすぐに適用します。,로컬 프록시 포트를 즉시 적용합니다.,Appliquer immédiatement le port proxy local. +Capture the screen and import the first supported Shadowsocks QR code found.,Снять изображение экрана и импортировать первый найденный поддерживаемый QR-код Shadowsocks.,捕获屏幕并导入找到的第一个受支持的 Shadowsocks 二维码。,擷取螢幕並匯入找到的第一個受支援 Shadowsocks QR Code。,画面をキャプチャし、最初に見つかった対応 Shadowsocks QR コードをインポートします。,화면을 캡처하고 발견된 첫 번째 지원 Shadowsocks QR 코드를 가져옵니다.,Capturer l’écran et importer le premier QR code Shadowsocks pris en charge trouvé. +Check GitHub for available updates now.,Проверить GitHub на наличие обновлений сейчас.,立即检查 GitHub 更新。,立即檢查 GitHub 更新。,GitHub で利用可能な更新を今すぐ確認します。,지금 GitHub에서 사용 가능한 업데이트를 확인합니다.,Vérifier maintenant les mises à jour disponibles sur GitHub. +Choose an image file and scan it for a Shadowsocks QR code.,Выбрать файл изображения и проверить его на наличие QR-кода Shadowsocks.,选择图像文件并扫描其中的 Shadowsocks 二维码。,選擇影像檔並掃描其中的 Shadowsocks QR Code。,画像ファイルを選択し、Shadowsocks QR コードをスキャンします。,이미지 파일을 선택하고 Shadowsocks QR 코드를 스캔합니다.,Choisir un fichier image et rechercher un QR code Shadowsocks. +Choose whether Shadowsocks follows the Windows theme or always uses light or dark mode.,"Выбрать, следует ли Shadowsocks системной теме Windows или всегда использует светлую либо тёмную тему.",选择 Shadowsocks 跟随 Windows 主题,或始终使用浅色/深色模式。,選擇 Shadowsocks 跟隨 Windows 主題,或固定使用淺色/深色模式。,Shadowsocks が Windows テーマに従うか、常にライト/ダークモードを使うかを選択します。,"Shadowsocks가 Windows 테마를 따를지, 항상 밝은/어두운 모드를 사용할지 선택합니다.",Choisir si Shadowsocks suit le thème Windows ou utilise toujours le mode clair ou sombre. +"Choose whether matching traffic is proxied, sent directly, or blocked.","Выбрать, направлять ли совпавший трафик через прокси, напрямую или блокировать.",选择匹配的流量是走代理、直连还是阻止。,選擇符合的流量要經由 Proxy、直接連線或封鎖。,一致した通信をプロキシ、直接接続、またはブロックのどれにするか選択します。,"일치하는 트래픽을 프록시, 직접 연결 또는 차단할지 선택합니다.","Choisir si le trafic correspondant passe par le proxy, en direct ou est bloqué." +Clear the current log file and the text shown in this viewer.,Очистить текущий файл журнала и отображаемый в этом окне текст.,清除当前日志文件以及此查看器中显示的文本。,清除目前的記錄檔以及此檢視器中顯示的文字。,現在のログファイルと、このビューアに表示されているテキストを消去します。,현재 로그 파일과 이 뷰어에 표시된 텍스트를 지웁니다.,Effacer le fichier journal actuel et le texte affiché dans cette visionneuse. +"Configure PAC behavior, local PAC security and GeoSite sources.","Настроить PAC, защиту локального PAC и источники GeoSite.",配置 PAC 行为、本地 PAC 安全性和 GeoSite 源。,設定 PAC 行為、本機 PAC 安全性與 GeoSite 來源。,PAC の動作、ローカル PAC のセキュリティ、GeoSite ソースを設定します。,"PAC 동작, 로컬 PAC 보안 및 GeoSite 소스를 구성합니다.","Configurer le comportement PAC, la sécurité du PAC local et les sources GeoSite." +Configure Windows to use this Shadowsocks instance as the system proxy.,Настроить Windows на использование этого экземпляра Shadowsocks как системного прокси.,配置 Windows 使用此 Shadowsocks 实例作为系统代理。,設定 Windows 使用此 Shadowsocks 執行個體作為系統 Proxy。,Windows がこの Shadowsocks インスタンスをシステムプロキシとして使用するよう設定します。,Windows가 이 Shadowsocks 인스턴스를 시스템 프록시로 사용하도록 구성합니다.,Configurer Windows pour utiliser cette instance de Shadowsocks comme proxy système. +Configure an optional upstream proxy used to reach Shadowsocks servers.,Настроить необязательный вышестоящий прокси для подключения к серверам Shadowsocks.,配置用于连接 Shadowsocks 服务器的可选上游代理。,設定用來連線 Shadowsocks 伺服器的選用上游 Proxy。,Shadowsocks サーバーへの接続に使用する任意の上流プロキシを設定します。,Shadowsocks 서버에 연결할 때 사용할 선택적 업스트림 프록시를 구성합니다.,Configurer un proxy amont facultatif utilisé pour atteindre les serveurs Shadowsocks. +"Configure capture mode, Windows proxy settings and per-application routing.","Настроить режим перехвата, системный прокси Windows и маршрутизацию по приложениям.",配置捕获模式、Windows 代理设置和按应用程序路由。,設定擷取模式、Windows Proxy 設定與個別應用程式路由。,キャプチャモード、Windows プロキシ設定、アプリケーションごとのルーティングを設定します。,"캡처 모드, Windows 프록시 설정 및 애플리케이션별 라우팅을 구성합니다.","Configurer le mode de capture, les paramètres proxy Windows et le routage par application." +Configure global keyboard shortcuts for common Shadowsocks actions.,Настроить глобальные сочетания клавиш для основных действий Shadowsocks.,配置常用 Shadowsocks 操作的全局键盘快捷键。,設定常用 Shadowsocks 動作的全域鍵盤快速鍵。,一般的な Shadowsocks 操作用のグローバルキーボードショートカットを設定します。,자주 사용하는 Shadowsocks 작업의 전역 키보드 단축키를 구성합니다.,Configurer des raccourcis clavier globaux pour les actions Shadowsocks courantes. +"Connect to the Shadowsocks server directly, without an upstream proxy.","Подключаться к серверу Shadowsocks напрямую, без вышестоящего прокси.",直接连接 Shadowsocks 服务器,不使用上游代理。,直接連線 Shadowsocks 伺服器,不使用上游 Proxy。,上流プロキシを使わず Shadowsocks サーバーに直接接続します。,업스트림 프록시 없이 Shadowsocks 서버에 직접 연결합니다.,"Se connecter directement au serveur Shadowsocks, sans proxy amont." +Connect to the Shadowsocks server through an upstream HTTP proxy.,Подключаться к серверу Shadowsocks через вышестоящий HTTP-прокси.,通过上游 HTTP 代理连接 Shadowsocks 服务器。,透過上游 HTTP Proxy 連線 Shadowsocks 伺服器。,上流 HTTP プロキシ経由で Shadowsocks サーバーに接続します。,업스트림 HTTP 프록시를 통해 Shadowsocks 서버에 연결합니다.,Se connecter au serveur Shadowsocks via un proxy HTTP amont. +Connect to the Shadowsocks server through an upstream SOCKS5 proxy.,Подключаться к серверу Shadowsocks через вышестоящий SOCKS5-прокси.,通过上游 SOCKS5 代理连接 Shadowsocks 服务器。,透過上游 SOCKS5 Proxy 連線 Shadowsocks 伺服器。,上流 SOCKS5 プロキシ経由で Shadowsocks サーバーに接続します。,업스트림 SOCKS5 프록시를 통해 Shadowsocks 서버에 연결합니다.,Se connecter au serveur Shadowsocks via un proxy SOCKS5 amont. +"Connection timeout for the upstream proxy, in seconds.",Тайм-аут подключения к вышестоящему прокси в секундах.,上游代理连接超时(秒)。,上游 Proxy 連線逾時(秒)。,上流プロキシの接続タイムアウト(秒)。,업스트림 프록시 연결 제한 시간(초)입니다.,"Délai de connexion au proxy amont, en secondes." +"Connection timeout for this server, in seconds.",Тайм-аут подключения к этому серверу в секундах.,此服务器的连接超时(秒)。,此伺服器的連線逾時(秒)。,このサーバーの接続タイムアウト(秒)。,이 서버의 연결 제한 시간(초)입니다.,"Délai de connexion pour ce serveur, en secondes." +Copy the local PAC endpoint URL to the clipboard.,Скопировать URL локального PAC в буфер обмена.,将本地 PAC 端点 URL 复制到剪贴板。,將本機 PAC 端點 URL 複製到剪貼簿。,ローカル PAC エンドポイント URL をクリップボードにコピーします。,로컬 PAC 엔드포인트 URL을 클립보드에 복사합니다.,Copier l’URL du point de terminaison PAC local dans le Presse-papiers. +Copy the selected online configuration URL to the clipboard.,Скопировать URL выбранной онлайн-конфигурации в буфер обмена.,将选中的在线配置 URL 复制到剪贴板。,將選取的線上設定 URL 複製到剪貼簿。,選択したオンライン設定 URL をクリップボードにコピーします。,선택한 온라인 구성 URL을 클립보드에 복사합니다.,Copier l’URL de configuration en ligne sélectionnée dans le Presse-papiers. +Copy the selected server as an ss:// URL.,Скопировать выбранный сервер в виде URL ss://.,将选中的服务器复制为 ss:// URL。,將選取的伺服器複製為 ss:// URL。,選択したサーバーを ss:// URL としてコピーします。,선택한 서버를 ss:// URL로 복사합니다.,Copier le serveur sélectionné sous forme d’URL ss://. +Create a copy of the selected server.,Создать копию выбранного сервера.,创建选中服务器的副本。,建立所選伺服器的副本。,選択したサーバーのコピーを作成します。,선택한 서버의 복사본을 만듭니다.,Créer une copie du serveur sélectionné. +Create a new server entry.,Создать новую запись сервера.,创建新的服务器条目。,建立新的伺服器項目。,新しいサーバーエントリを作成します。,새 서버 항목을 만듭니다.,Créer une nouvelle entrée de serveur. +Delete the selected server.,Удалить выбранный сервер.,删除选中的服务器。,刪除選取的伺服器。,選択したサーバーを削除します。,선택한 서버를 삭제합니다.,Supprimer le serveur sélectionné. +Discard unsaved Game Mode application changes.,Отменить несохранённые изменения списка приложений игрового режима.,丢弃未保存的游戏模式应用程序更改。,捨棄未儲存的遊戲模式應用程式變更。,未保存の Game Mode アプリケーション変更を破棄します。,저장하지 않은 Game Mode 애플리케이션 변경 내용을 버립니다.,Ignorer les modifications non enregistrées des applications du Game Mode. +Discard unsaved forward-proxy changes.,Отменить несохранённые изменения вышестоящего прокси.,丢弃未保存的上游代理更改。,捨棄未儲存的上游 Proxy 變更。,未保存の上流プロキシ変更を破棄します。,저장하지 않은 업스트림 프록시 변경 내용을 버립니다.,Ignorer les modifications non enregistrées du proxy amont. +Discard unsaved hotkey edits and reload the saved shortcuts.,Отменить несохранённые изменения горячих клавиш и загрузить сохранённые сочетания.,丢弃未保存的快捷键修改并重新加载已保存的快捷键。,捨棄未儲存的快速鍵變更並重新載入已儲存的快速鍵。,未保存のホットキー編集を破棄し、保存済みショートカットを再読み込みします。,저장하지 않은 단축키 편집을 버리고 저장된 단축키를 다시 불러옵니다.,Ignorer les modifications de raccourcis non enregistrées et recharger les raccourcis sauvegardés. +Discard unsaved routing-rule changes and reload the saved configuration.,Отменить несохранённые изменения правил маршрутизации и загрузить сохранённую конфигурацию.,丢弃未保存的路由规则更改并重新加载已保存配置。,捨棄未儲存的路由規則變更並重新載入已儲存設定。,未保存のルーティングルール変更を破棄し、保存済み設定を再読み込みします。,저장하지 않은 라우팅 규칙 변경 내용을 버리고 저장된 구성을 다시 불러옵니다.,Ignorer les modifications de règles de routage non enregistrées et recharger la configuration sauvegardée. +Download and apply every configured online source.,Загрузить и применить все настроенные онлайн-источники.,下载并应用所有已配置的在线源。,下載並套用所有已設定的線上來源。,設定済みのオンラインソースをすべてダウンロードして適用します。,구성된 모든 온라인 소스를 다운로드하고 적용합니다.,Télécharger et appliquer toutes les sources en ligne configurées. +Download and apply the selected online configuration source now.,Загрузить и применить выбранный источник онлайн-конфигурации сейчас.,立即下载并应用选中的在线配置源。,立即下載並套用選取的線上設定來源。,選択したオンライン設定ソースを今すぐダウンロードして適用します。,선택한 온라인 구성 소스를 지금 다운로드하고 적용합니다.,Télécharger et appliquer maintenant la source de configuration en ligne sélectionnée. +Download the configured GeoSite data now and rebuild dependent local PAC data.,Загрузить настроенные данные GeoSite и пересобрать зависящий от них локальный PAC.,立即下载已配置的 GeoSite 数据并重建相关本地 PAC 数据。,立即下載已設定的 GeoSite 資料並重建相依的本機 PAC 資料。,設定済み GeoSite データを今すぐダウンロードし、関連するローカル PAC データを再構築します。,구성된 GeoSite 데이터를 지금 다운로드하고 종속 로컬 PAC 데이터를 다시 생성합니다.,Télécharger maintenant les données GeoSite configurées et reconstruire les données PAC locales dépendantes. +Download the selected release asset to the application update workspace.,Загрузить файл выбранного релиза во временную область обновления приложения.,将选中的发行资源下载到应用更新工作区。,將選取的版本資產下載到應用程式更新工作區。,選択したリリースアセットをアプリ更新ワークスペースにダウンロードします。,선택한 릴리스 자산을 애플리케이션 업데이트 작업 공간에 다운로드합니다.,Télécharger l’asset de version sélectionné dans l’espace de travail de mise à jour de l’application. +Enable an additional command-line argument string for the plugin.,Разрешить дополнительную строку аргументов командной строки для плагина.,为插件启用额外的命令行参数字符串。,為外掛程式啟用額外的命令列參數字串。,プラグイン用の追加コマンドライン引数文字列を有効にします。,플러그인에 추가 명령줄 인수 문자열을 사용하도록 설정합니다.,Activer une chaîne d’arguments de ligne de commande supplémentaire pour le plugin. +Enable debug-level application logging. This can significantly increase log volume.,Включить журналирование уровня Debug. Это может существенно увеличить объём журнала.,启用调试级应用日志。这可能会显著增加日志量。,啟用偵錯層級的應用程式記錄。這可能會大幅增加記錄量。,デバッグレベルのアプリケーションログを有効にします。ログ量が大幅に増える場合があります。,디버그 수준 애플리케이션 로깅을 활성화합니다. 로그 양이 크게 늘어날 수 있습니다.,Activer la journalisation de niveau débogage. Cela peut augmenter considérablement le volume des journaux. +Enable or disable this application routing rule without deleting it.,Включить или отключить это правило маршрутизации приложения без его удаления.,启用或禁用此应用程序路由规则而不删除它。,啟用或停用此應用程式路由規則而不刪除它。,このアプリケーションルーティングルールを削除せずに有効/無効にします。,이 애플리케이션 라우팅 규칙을 삭제하지 않고 활성화하거나 비활성화합니다.,Activer ou désactiver cette règle de routage d’application sans la supprimer. +Encryption method negotiated with the selected Shadowsocks server.,"Метод шифрования, используемый выбранным сервером Shadowsocks.",与所选 Shadowsocks 服务器协商的加密方式。,與所選 Shadowsocks 伺服器協商的加密方式。,選択した Shadowsocks サーバーとネゴシエートする暗号化方式です。,선택한 Shadowsocks 서버와 협상되는 암호화 방식입니다.,Méthode de chiffrement négociée avec le serveur Shadowsocks sélectionné. +Enter an absolute HTTP or HTTPS SIP008 / online configuration URL.,Введите абсолютный HTTP- или HTTPS-URL SIP008 / онлайн-конфигурации.,输入绝对 HTTP 或 HTTPS SIP008 / 在线配置 URL。,輸入絕對 HTTP 或 HTTPS SIP008 / 線上設定 URL。,絶対 HTTP または HTTPS の SIP008 / オンライン設定 URL を入力します。,절대 HTTP 또는 HTTPS SIP008 / 온라인 구성 URL을 입력합니다.,Saisir une URL absolue HTTP ou HTTPS de configuration SIP008 / en ligne. +"Enter an executable name, full path, or wildcard pattern to trigger automatic Game Mode.","Введите имя исполняемого файла, полный путь или шаблон для автоматического включения игрового режима.",输入可执行文件名、完整路径或通配符模式以触发自动游戏模式。,輸入執行檔名稱、完整路徑或萬用字元模式以觸發自動遊戲模式。,自動 Game Mode を起動する実行ファイル名、完全パス、またはワイルドカードパターンを入力します。,"자동 Game Mode를 트리거할 실행 파일 이름, 전체 경로 또는 와일드카드 패턴을 입력합니다.","Saisir un nom d’exécutable, un chemin complet ou un motif générique pour déclencher le Game Mode automatique." +"Enter one or more GeoSite source URLs, one per line.","Введите один или несколько URL источников GeoSite, по одному на строку.",输入一个或多个 GeoSite 源 URL,每行一个。,輸入一個或多個 GeoSite 來源 URL,每行一個。,1行に1つ、1つ以上の GeoSite ソース URL を入力します。,GeoSite 소스 URL을 한 줄에 하나씩 입력합니다.,"Saisir une ou plusieurs URL de source GeoSite, une par ligne." +"Executable name, full path, or wildcard pattern used to match a process.","Имя исполняемого файла, полный путь или шаблон для сопоставления с процессом.",用于匹配进程的可执行文件名、完整路径或通配符模式。,用於比對處理程序的執行檔名稱、完整路徑或萬用字元模式。,プロセス照合に使用する実行ファイル名、完全パス、またはワイルドカードパターンです。,"프로세스를 일치시키는 데 사용할 실행 파일 이름, 전체 경로 또는 와일드카드 패턴입니다.","Nom d’exécutable, chemin complet ou motif générique utilisé pour faire correspondre un processus." +Explain how closing or hiding the main window differs from exiting the tray application.,"Показать, чем закрытие или скрытие главного окна отличается от полного выхода из приложения в трее.",说明关闭或隐藏主窗口与从托盘退出应用程序之间的区别。,說明關閉或隱藏主視窗與從系統匣結束應用程式之間的差異。,メインウィンドウを閉じる/隠す操作と、トレイからアプリを終了する操作の違いを説明します。,기본 창을 닫거나 숨기는 것과 트레이에서 애플리케이션을 종료하는 것의 차이를 설명합니다.,Expliquer la différence entre fermer ou masquer la fenêtre principale et quitter l’application depuis la zone de notification. +Focus this field and press the desired modifier keys plus a key. Press Esc to clear it.,"Выберите это поле и нажмите нужные модификаторы и клавишу. Нажмите Esc, чтобы очистить сочетание.",聚焦此字段,然后按所需的修饰键和按键。按 Esc 清除。,聚焦此欄位,然後按所需的修飾鍵與按鍵。按 Esc 清除。,この欄にフォーカスし、目的の修飾キーとキーを押します。Esc でクリアします。,이 필드에 포커스를 두고 원하는 수정 키와 키를 누르세요. Esc를 누르면 지워집니다.,Placez le focus dans ce champ et appuyez sur les modificateurs et la touche souhaités. Appuyez sur Esc pour effacer. +"Friendly name shown in server lists. If left empty, Shadowsocks assigns a unique Server N name.","Понятное имя, отображаемое в списках серверов. Если оставить поле пустым, Shadowsocks назначит уникальное имя Server N.",服务器列表中显示的友好名称。留空时,Shadowsocks 会分配唯一的 Server N 名称。,伺服器清單中顯示的易讀名稱。留空時,Shadowsocks 會指派唯一的 Server N 名稱。,サーバー一覧に表示する分かりやすい名前です。空欄の場合、Shadowsocks が一意の Server N 名を割り当てます。,서버 목록에 표시되는 친숙한 이름입니다. 비워 두면 Shadowsocks가 고유한 Server N 이름을 할당합니다.,"Nom convivial affiché dans les listes de serveurs. S’il est vide, Shadowsocks attribue un nom unique de type Server N." +Hide the current update notification without skipping the release.,"Скрыть текущее уведомление об обновлении, не пропуская этот релиз.",隐藏当前更新通知,但不跳过此版本。,隱藏目前的更新通知,但不略過此版本。,このリリースをスキップせず、現在の更新通知を隠します。,이 릴리스를 건너뛰지 않고 현재 업데이트 알림을 숨깁니다.,Masquer la notification de mise à jour actuelle sans ignorer cette version. +Hostname or IP address of the Shadowsocks server.,Имя хоста или IP-адрес сервера Shadowsocks.,Shadowsocks 服务器的主机名或 IP 地址。,Shadowsocks 伺服器的主機名稱或 IP 位址。,Shadowsocks サーバーのホスト名または IP アドレスです。,Shadowsocks 서버의 호스트 이름 또는 IP 주소입니다.,Nom d’hôte ou adresse IP du serveur Shadowsocks. +Hostname or IP address of the upstream proxy.,Имя хоста или IP-адрес вышестоящего прокси.,上游代理的主机名或 IP 地址。,上游 Proxy 的主機名稱或 IP 位址。,上流プロキシのホスト名または IP アドレスです。,업스트림 프록시의 호스트 이름 또는 IP 주소입니다.,Nom d’hôte ou adresse IP du proxy amont. +Ignore this release in future automatic update checks.,Игнорировать этот релиз при последующих автоматических проверках обновлений.,在以后的自动更新检查中忽略此版本。,在之後的自動更新檢查中略過此版本。,今後の自動更新確認でこのリリースを無視します。,향후 자동 업데이트 확인에서 이 릴리스를 무시합니다.,Ignorer cette version lors des futures vérifications automatiques de mises à jour. +Import every valid ss:// link from the text box.,Импортировать все корректные ссылки ss:// из текстового поля.,从文本框导入所有有效的 ss:// 链接。,從文字方塊匯入所有有效的 ss:// 連結。,テキストボックス内の有効な ss:// リンクをすべてインポートします。,텍스트 상자의 유효한 ss:// 링크를 모두 가져옵니다.,Importer tous les liens ss:// valides depuis la zone de texte. +Include SIP003 plugin stdout and stderr events in the Shadowsocks log.,Добавлять stdout и stderr событий SIP003-плагина в журнал Shadowsocks.,在 Shadowsocks 日志中包含 SIP003 插件的 stdout 和 stderr 事件。,在 Shadowsocks 記錄中包含 SIP003 外掛程式的 stdout 與 stderr 事件。,Shadowsocks ログに SIP003 プラグインの stdout / stderr イベントを含めます。,Shadowsocks 로그에 SIP003 플러그인의 stdout 및 stderr 이벤트를 포함합니다.,Inclure les événements stdout et stderr des plugins SIP003 dans le journal Shadowsocks. +Include prerelease builds when checking GitHub for updates.,Учитывать предварительные версии при проверке обновлений на GitHub.,检查 GitHub 更新时包含预发布版本。,檢查 GitHub 更新時包含預發行版本。,GitHub の更新確認時にプレリリースビルドを含めます。,GitHub 업데이트 확인 시 사전 릴리스 빌드를 포함합니다.,Inclure les versions préliminaires lors de la recherche de mises à jour GitHub. +Keep the main Shadowsocks window above other windows while viewing logs.,Держать главное окно Shadowsocks поверх остальных окон при просмотре журнала.,查看日志时使 Shadowsocks 主窗口保持在其他窗口之上。,檢視記錄時讓 Shadowsocks 主視窗保持在其他視窗上方。,ログ表示中、Shadowsocks のメインウィンドウを他のウィンドウより前面に保ちます。,로그를 보는 동안 Shadowsocks 기본 창을 다른 창 위에 유지합니다.,Maintenir la fenêtre principale de Shadowsocks au-dessus des autres fenêtres lors de la consultation des journaux. +Local SOCKS/HTTP listening port used by Shadowsocks.,"Локальный порт SOCKS/HTTP, который прослушивает Shadowsocks.",Shadowsocks 使用的本地 SOCKS/HTTP 监听端口。,Shadowsocks 使用的本機 SOCKS/HTTP 接聽連接埠。,Shadowsocks が使用するローカル SOCKS/HTTP 待受ポートです。,Shadowsocks가 사용하는 로컬 SOCKS/HTTP 수신 포트입니다.,Port d’écoute SOCKS/HTTP local utilisé par Shadowsocks. +Local SOCKS/HTTP port used by applications and the Windows proxy configuration.,"Локальный порт SOCKS/HTTP, используемый приложениями и системным прокси Windows.",应用程序和 Windows 代理配置使用的本地 SOCKS/HTTP 端口。,應用程式與 Windows Proxy 設定使用的本機 SOCKS/HTTP 連接埠。,アプリケーションと Windows プロキシ設定で使用するローカル SOCKS/HTTP ポートです。,애플리케이션 및 Windows 프록시 구성에서 사용하는 로컬 SOCKS/HTTP 포트입니다.,Port SOCKS/HTTP local utilisé par les applications et la configuration proxy Windows. +Manage SIP008 and other online server configuration sources.,Управлять SIP008 и другими источниками онлайн-конфигурации серверов.,管理 SIP008 和其他在线服务器配置源。,管理 SIP008 與其他線上伺服器設定來源。,SIP008 およびその他のオンラインサーバー設定ソースを管理します。,SIP008 및 기타 온라인 서버 구성 소스를 관리합니다.,Gérer SIP008 et les autres sources de configuration de serveurs en ligne. +Manage Shadowsocks servers and local client connection settings.,Управлять серверами Shadowsocks и параметрами локального клиента.,管理 Shadowsocks 服务器和本地客户端连接设置。,管理 Shadowsocks 伺服器與本機用戶端連線設定。,Shadowsocks サーバーとローカルクライアント接続設定を管理します。,Shadowsocks 서버 및 로컬 클라이언트 연결 설정을 관리합니다.,Gérer les serveurs Shadowsocks et les paramètres de connexion du client local. +Manage applications that automatically suspend WinDivert while they are running.,"Управлять приложениями, при работе которых WinDivert автоматически приостанавливается.",管理运行时自动暂停 WinDivert 的应用程序。,管理執行時會自動暫停 WinDivert 的應用程式。,実行中に WinDivert を自動停止するアプリケーションを管理します。,실행 중 WinDivert를 자동으로 일시 중지하는 애플리케이션을 관리합니다.,Gérer les applications qui suspendent automatiquement WinDivert pendant leur exécution. +Move the selected GeoSite source one position down.,Переместить выбранный источник GeoSite на одну позицию вниз.,将选中的 GeoSite 源下移一位。,將選取的 GeoSite 來源下移一個位置。,選択した GeoSite ソースを1つ下へ移動します。,선택한 GeoSite 소스를 한 단계 아래로 이동합니다.,Déplacer la source GeoSite sélectionnée d’une position vers le bas. +Move the selected GeoSite source one position up.,Переместить выбранный источник GeoSite на одну позицию вверх.,将选中的 GeoSite 源上移一位。,將選取的 GeoSite 來源上移一個位置。,選択した GeoSite ソースを1つ上へ移動します。,선택한 GeoSite 소스를 한 단계 위로 이동합니다.,Déplacer la source GeoSite sélectionnée d’une position vers le haut. +Move the selected server one position down.,Переместить выбранный сервер на одну позицию вниз.,将选中的服务器下移一位。,將選取的伺服器下移一個位置。,選択したサーバーを1つ下へ移動します。,선택한 서버를 한 단계 아래로 이동합니다.,Déplacer le serveur sélectionné d’une position vers le bas. +Move the selected server one position up.,Переместить выбранный сервер на одну позицию вверх.,将选中的服务器上移一位。,將選取的伺服器上移一個位置。,選択したサーバーを1つ上へ移動します。,선택한 서버를 한 단계 위로 이동합니다.,Déplacer le serveur sélectionné d’une position vers le haut. +Open Share / QR for the selected server.,Открыть раздел «Поделиться / QR» для выбранного сервера.,为选中的服务器打开“共享 / QR”。,為選取的伺服器開啟「分享 / QR」。,選択したサーバーの「共有 / QR」を開きます。,선택한 서버의 공유 / QR을 엽니다.,Ouvrir Partage / QR pour le serveur sélectionné. +Open the GitHub issue tracker for bug reports and feature requests.,Открыть GitHub Issues для сообщений об ошибках и предложений функций.,在默认浏览器中打开 GitHub issue 跟踪器,用于错误报告和功能请求。,在預設瀏覽器中開啟 GitHub Issue 追蹤器,以回報錯誤與提出功能需求。,既定のブラウザーで GitHub の Issue トラッカーを開き、バグ報告や機能要望を送ります。,기본 브라우저에서 GitHub 이슈 트래커를 열어 버그 보고 및 기능 요청을 합니다.,Ouvrir le suivi des issues GitHub dans le navigateur par défaut pour les rapports de bugs et les demandes de fonctionnalités. +Open the Shadowsocks Reborn project page in your default browser.,Открыть страницу проекта Shadowsocks Reborn в браузере по умолчанию.,在默认浏览器中打开 Shadowsocks Reborn 项目页面。,在預設瀏覽器中開啟 Shadowsocks Reborn 專案頁面。,既定のブラウザーで Shadowsocks Reborn のプロジェクトページを開きます。,기본 브라우저에서 Shadowsocks Reborn 프로젝트 페이지를 엽니다.,Ouvrir la page du projet Shadowsocks Reborn dans le navigateur par défaut. +Open the active log file with the default Windows application.,Открыть активный файл журнала приложением Windows по умолчанию.,使用 Windows 默认应用程序打开当前日志文件。,使用 Windows 預設應用程式開啟目前記錄檔。,現在のログファイルを Windows の既定アプリで開きます。,현재 로그 파일을 기본 Windows 애플리케이션으로 엽니다.,Ouvrir le fichier journal actif avec l’application Windows par défaut. +Open the connection overview and current runtime status.,Открыть обзор подключения и текущего состояния приложения.,打开连接概览和当前运行状态。,開啟連線概覽與目前執行狀態。,接続の概要と現在の実行状態を開きます。,연결 개요와 현재 런타임 상태를 엽니다.,Ouvrir l’aperçu de la connexion et l’état d’exécution actuel. +"Open the folder containing settings, logs, caches and runtime data for the current mode.","Открыть папку с настройками, журналами, кэшами и служебными данными текущего режима.",打开包含当前模式的设置、日志、缓存和运行时数据的文件夹。,開啟包含目前模式設定、記錄、快取與執行階段資料的資料夾。,現在のモードの設定、ログ、キャッシュ、ランタイムデータを含むフォルダーを開きます。,"현재 모드의 설정, 로그, 캐시 및 런타임 데이터가 있는 폴더를 엽니다.","Ouvrir le dossier contenant les paramètres, journaux, caches et données d’exécution du mode actuel." +Open the folder that contains Shadowsocks log files.,Открыть папку с файлами журнала Shadowsocks.,打开包含 Shadowsocks 日志文件的文件夹。,開啟包含 Shadowsocks 記錄檔的資料夾。,Shadowsocks のログファイルがあるフォルダーを開きます。,Shadowsocks 로그 파일이 있는 폴더를 엽니다.,Ouvrir le dossier contenant les fichiers journaux Shadowsocks. +Optional SIP003 plugin executable name or absolute path.,Необязательное имя исполняемого файла SIP003-плагина или абсолютный путь к нему.,可选的 SIP003 插件可执行文件名或绝对路径。,選用的 SIP003 外掛程式執行檔名稱或絕對路徑。,任意の SIP003 プラグイン実行ファイル名または絶対パスです。,선택적 SIP003 플러그인 실행 파일 이름 또는 절대 경로입니다.,Nom d’exécutable ou chemin absolu facultatif du plugin SIP003. +Optional password for upstream proxy authentication.,Необязательный пароль для аутентификации на вышестоящем прокси.,上游代理认证的可选密码。,上游 Proxy 驗證的選用密碼。,上流プロキシ認証用の任意のパスワードです。,업스트림 프록시 인증을 위한 선택적 비밀번호입니다.,Mot de passe facultatif pour l’authentification du proxy amont. +Optional username for upstream proxy authentication.,Необязательное имя пользователя для аутентификации на вышестоящем прокси.,上游代理认证的可选用户名。,上游 Proxy 驗證的選用使用者名稱。,上流プロキシ認証用の任意のユーザー名です。,업스트림 프록시 인증을 위한 선택적 사용자 이름입니다.,Nom d’utilisateur facultatif pour l’authentification du proxy amont. +Options passed to the SIP003 plugin through SS_PLUGIN_OPTIONS.,"Параметры, передаваемые SIP003-плагину через SS_PLUGIN_OPTIONS.",通过 SS_PLUGIN_OPTIONS 传递给 SIP003 插件的选项。,透過 SS_PLUGIN_OPTIONS 傳遞給 SIP003 外掛程式的選項。,SS_PLUGIN_OPTIONS を通じて SIP003 プラグインに渡すオプションです。,SS_PLUGIN_OPTIONS를 통해 SIP003 플러그인에 전달되는 옵션입니다.,Options transmises au plugin SIP003 via SS_PLUGIN_OPTIONS. +Password used by this Shadowsocks server configuration.,"Пароль, используемый этой конфигурацией сервера Shadowsocks.",此 Shadowsocks 服务器配置使用的密码。,此 Shadowsocks 伺服器設定使用的密碼。,この Shadowsocks サーバー設定で使用するパスワードです。,이 Shadowsocks 서버 구성에서 사용하는 비밀번호입니다.,Mot de passe utilisé par cette configuration de serveur Shadowsocks. +"Paste one or more ss:// server links, one per line.","Вставьте одну или несколько ссылок серверов ss://, по одной на строку.",粘贴一个或多个 ss:// 服务器链接,每行一个。,貼上一個或多個 ss:// 伺服器連結,每行一個。,1行に1つ、1つ以上の ss:// サーバーリンクを貼り付けます。,ss:// 서버 링크를 한 줄에 하나씩 붙여넣습니다.,"Coller un ou plusieurs liens de serveur ss://, un par ligne." +Paste text from the clipboard into the ss:// import field.,Вставить текст из буфера обмена в поле импорта ss://.,将剪贴板文本粘贴到 ss:// 导入字段。,將剪貼簿文字貼到 ss:// 匯入欄位。,クリップボードのテキストを ss:// インポート欄に貼り付けます。,클립보드 텍스트를 ss:// 가져오기 필드에 붙여넣습니다.,Coller le texte du Presse-papiers dans le champ d’importation ss://. +"Read-only group supplied by an imported or online configuration, when available.","Группа только для чтения, полученная из импортированной или онлайн-конфигурации, если она задана.",导入或在线配置提供的只读组(如果有)。,匯入或線上設定提供的唯讀群組(如果有)。,インポートまたはオンライン設定から提供された読み取り専用グループです(利用可能な場合)。,가져오기 또는 온라인 구성에서 제공하는 읽기 전용 그룹입니다(사용 가능한 경우).,"Groupe en lecture seule fourni par une importation ou une configuration en ligne, s’il est disponible." +Rebuild the local PAC automatically after application or GeoSite data updates.,Автоматически пересобирать локальный PAC после обновления приложения или данных GeoSite.,应用或 GeoSite 数据更新后自动重建本地 PAC。,應用程式或 GeoSite 資料更新後自動重建本機 PAC。,アプリケーションまたは GeoSite データの更新後にローカル PAC を自動再構築します。,애플리케이션 또는 GeoSite 데이터 업데이트 후 로컬 PAC를 자동으로 다시 생성합니다.,Reconstruire automatiquement le PAC local après les mises à jour de l’application ou des données GeoSite. +Refresh the log viewer automatically every two seconds.,Автоматически обновлять просмотр журнала каждые две секунды.,每两秒自动刷新日志查看器。,每兩秒自動重新整理記錄檢視器。,ログビューアを2秒ごとに自動更新します。,로그 뷰어를 2초마다 자동으로 새로 고칩니다.,Actualiser automatiquement la visionneuse de journaux toutes les deux secondes. +Register the configured global hotkeys automatically when Shadowsocks starts.,Автоматически регистрировать настроенные глобальные горячие клавиши при запуске Shadowsocks.,Shadowsocks 启动时自动注册已配置的全局快捷键。,Shadowsocks 啟動時自動註冊已設定的全域快速鍵。,Shadowsocks 起動時に設定済みグローバルホットキーを自動登録します。,Shadowsocks 시작 시 구성된 전역 단축키를 자동으로 등록합니다.,Enregistrer automatiquement les raccourcis globaux configurés au démarrage de Shadowsocks. +Reload the latest log contents immediately.,Немедленно перечитать последние данные журнала.,立即重新加载最新日志内容。,立即重新載入最新記錄內容。,最新のログ内容をすぐに再読み込みします。,최신 로그 내용을 즉시 다시 불러옵니다.,Recharger immédiatement le contenu le plus récent du journal. +Remove the selected GeoSite source from the list.,Удалить выбранный источник GeoSite из списка.,从列表中删除选中的 GeoSite 源。,從清單中移除選取的 GeoSite 來源。,選択した GeoSite ソースを一覧から削除します。,선택한 GeoSite 소스를 목록에서 제거합니다.,Supprimer la source GeoSite sélectionnée de la liste. +Remove the selected online configuration source.,Удалить выбранный источник онлайн-конфигурации.,删除选中的在线配置源。,移除選取的線上設定來源。,選択したオンライン設定ソースを削除します。,선택한 온라인 구성 소스를 제거합니다.,Supprimer la source de configuration en ligne sélectionnée. +Remove this application from the automatic Game Mode list.,Удалить это приложение из списка автоматического игрового режима.,从自动游戏模式列表中删除此应用程序。,從自動遊戲模式清單中移除此應用程式。,このアプリケーションを自動 Game Mode 一覧から削除します。,이 애플리케이션을 자동 Game Mode 목록에서 제거합니다.,Supprimer cette application de la liste automatique du Game Mode. +Remove this application routing rule.,Удалить это правило маршрутизации приложения.,删除此应用程序路由规则。,移除此應用程式路由規則。,このアプリケーションルーティングルールを削除します。,이 애플리케이션 라우팅 규칙을 제거합니다.,Supprimer cette règle de routage d’application. +Request elevation and enable transparent TCP/UDP capture through NetworkService and WinDivert.,Запросить повышение прав и включить прозрачный перехват TCP/UDP через NetworkService и WinDivert.,请求提升权限,并通过 NetworkService 和 WinDivert 启用透明 TCP/UDP 捕获。,要求提高權限,並透過 NetworkService 與 WinDivert 啟用透明 TCP/UDP 擷取。,昇格を要求し、NetworkService と WinDivert による透過 TCP/UDP キャプチャを有効にします。,권한 상승을 요청하고 NetworkService 및 WinDivert를 통한 투명 TCP/UDP 캡처를 활성화합니다.,Demander une élévation et activer la capture TCP/UDP transparente via NetworkService et WinDivert. +Require a random secret in the local PAC URL before the PAC file is served.,Требовать случайный секрет в URL локального PAC перед выдачей PAC-файла.,提供本地 PAC 文件前,要求本地 PAC URL 中包含随机密钥。,提供本機 PAC 檔案前,要求本機 PAC URL 中包含隨機密鑰。,ローカル PAC を配信する前に、ローカル PAC URL にランダムなシークレットを要求します。,로컬 PAC 파일을 제공하기 전에 로컬 PAC URL에 임의의 비밀값을 요구합니다.,Exiger un secret aléatoire dans l’URL PAC locale avant de servir le fichier PAC. +Restore the built-in default GeoSite source list.,Восстановить встроенный список источников GeoSite по умолчанию.,恢复内置的默认 GeoSite 源列表。,還原內建的預設 GeoSite 來源清單。,組み込みの既定 GeoSite ソース一覧を復元します。,기본 제공 GeoSite 소스 목록을 복원합니다.,Restaurer la liste intégrée des sources GeoSite par défaut. +Restore the selected server fields to their last saved values.,Вернуть поля выбранного сервера к последним сохранённым значениям.,将选中服务器字段恢复为上次保存的值。,將選取伺服器欄位還原為上次儲存的值。,選択したサーバーの各項目を最後に保存した値へ戻します。,선택한 서버 필드를 마지막으로 저장한 값으로 복원합니다.,Restaurer les champs du serveur sélectionné à leurs dernières valeurs enregistrées. +Save the configured Game Mode application list.,Сохранить настроенный список приложений игрового режима.,保存已配置的游戏模式应用程序列表。,儲存已設定的遊戲模式應用程式清單。,設定した Game Mode アプリケーション一覧を保存します。,구성된 Game Mode 애플리케이션 목록을 저장합니다.,Enregistrer la liste configurée des applications du Game Mode. +Save the current GeoSite source order and URLs.,Сохранить текущий порядок и URL источников GeoSite.,保存当前 GeoSite 源顺序和 URL。,儲存目前 GeoSite 來源順序與 URL。,現在の GeoSite ソースの順序と URL を保存します。,현재 GeoSite 소스 순서와 URL을 저장합니다.,Enregistrer l’ordre actuel des sources GeoSite et leurs URL. +Save the hotkey configuration and apply it to the running application.,Сохранить настройки горячих клавиш и применить их к запущенному приложению.,保存快捷键配置并应用到正在运行的应用程序。,儲存快速鍵設定並套用至執行中的應用程式。,ホットキー設定を保存し、実行中のアプリケーションに適用します。,단축키 구성을 저장하고 실행 중인 애플리케이션에 적용합니다.,Enregistrer la configuration des raccourcis et l’appliquer à l’application en cours d’exécution. +Save the online PAC URL and refresh the online PAC cache.,Сохранить URL онлайн-PAC и обновить его кэш.,保存在线 PAC URL 并刷新在线 PAC 缓存。,儲存線上 PAC URL 並重新整理線上 PAC 快取。,オンライン PAC URL を保存し、オンライン PAC キャッシュを更新します。,온라인 PAC URL을 저장하고 온라인 PAC 캐시를 새로 고칩니다.,Enregistrer l’URL PAC en ligne et actualiser le cache PAC en ligne. +"Scan Steam, Epic Games, GOG and Xbox libraries for installed games that can be added to automatic Game Mode.","Просканировать библиотеки Steam, Epic Games, GOG и Xbox на установленные игры, которые можно добавить в автоматический игровой режим.",扫描 Steam、Epic Games、GOG 和 Xbox 游戏库,查找可添加到自动游戏模式的已安装游戏。,掃描 Steam、Epic Games、GOG 與 Xbox 遊戲庫,尋找可新增至自動遊戲模式的已安裝遊戲。,Steam、Epic Games、GOG、Xbox のライブラリをスキャンし、自動 Game Mode に追加できるインストール済みゲームを探します。,"Steam, Epic Games, GOG 및 Xbox 라이브러리를 검색하여 자동 Game Mode에 추가할 수 있는 설치된 게임을 찾습니다.","Analyser les bibliothèques Steam, Epic Games, GOG et Xbox pour rechercher les jeux installés pouvant être ajoutés au Game Mode automatique." +Scan the image currently stored in the clipboard for a Shadowsocks QR code.,Проверить изображение из буфера обмена на наличие QR-кода Shadowsocks.,扫描剪贴板中的当前图像以查找 Shadowsocks 二维码。,掃描剪貼簿中的目前影像以尋找 Shadowsocks QR Code。,現在クリップボードにある画像から Shadowsocks QR コードをスキャンします。,현재 클립보드에 있는 이미지에서 Shadowsocks QR 코드를 스캔합니다.,Analyser l’image actuellement stockée dans le Presse-papiers à la recherche d’un QR code Shadowsocks. +"Select an online configuration source to update, copy, or remove it.","Выберите источник онлайн-конфигурации, чтобы обновить, скопировать или удалить его.",选择一个在线配置源进行更新、复制或删除。,選擇一個線上設定來源以更新、複製或移除。,更新、コピー、削除するオンライン設定ソースを選択します。,"업데이트, 복사 또는 제거할 온라인 구성 소스를 선택합니다.","Sélectionner une source de configuration en ligne à mettre à jour, copier ou supprimer." +Share servers and import ss:// links or QR codes.,Делиться серверами и импортировать ссылки ss:// или QR-коды.,分享服务器并导入 ss:// 链接或二维码。,分享伺服器並匯入 ss:// 連結或 QR Code。,サーバーを共有し、ss:// リンクまたは QR コードをインポートします。,서버를 공유하고 ss:// 링크 또는 QR 코드를 가져옵니다.,Partager des serveurs et importer des liens ss:// ou des QR codes. +Start Shadowsocks automatically when you sign in to Windows. This option is unavailable in Clean Mode.,Автоматически запускать Shadowsocks при входе в Windows. В Clean Mode эта опция недоступна.,登录 Windows 时自动启动 Shadowsocks。Clean Mode 下此选项不可用。,登入 Windows 時自動啟動 Shadowsocks。Clean Mode 下此選項無法使用。,Windows へのサインイン時に Shadowsocks を自動起動します。Clean Mode ではこのオプションは使用できません。,Windows 로그인 시 Shadowsocks를 자동으로 시작합니다. Clean Mode에서는 이 옵션을 사용할 수 없습니다.,Démarrer automatiquement Shadowsocks à la connexion à Windows. Cette option n’est pas disponible en Clean Mode. +Switch between an online PAC URL and the locally generated GeoSite PAC.,Переключаться между URL онлайн-PAC и локально созданным PAC на основе GeoSite.,在在线 PAC URL 与本地生成的 GeoSite PAC 之间切换。,在線上 PAC URL 與本機產生的 GeoSite PAC 之間切換。,オンライン PAC URL とローカル生成の GeoSite PAC を切り替えます。,온라인 PAC URL과 로컬로 생성된 GeoSite PAC 간에 전환합니다.,Basculer entre une URL PAC en ligne et le PAC GeoSite généré localement. +TCP port of the upstream proxy.,TCP-порт вышестоящего прокси.,上游代理的 TCP 端口。,上游 Proxy 的 TCP 連接埠。,上流プロキシの TCP ポートです。,업스트림 프록시의 TCP 포트입니다.,Port TCP du proxy amont. +TCP/UDP port exposed by the Shadowsocks server.,TCP/UDP-порт сервера Shadowsocks.,Shadowsocks 服务器公开的 TCP/UDP 端口。,Shadowsocks 伺服器公開的 TCP/UDP 連接埠。,Shadowsocks サーバーが公開する TCP/UDP ポートです。,Shadowsocks 서버가 노출하는 TCP/UDP 포트입니다.,Port TCP/UDP exposé par le serveur Shadowsocks. +Temporarily reveal the password for manually managed servers. Link-managed servers keep this option hidden.,"Временно показать пароль для серверов, управляемых вручную. Для серверов из ссылок эта опция скрыта.",临时显示手动管理服务器的密码。通过链接管理的服务器会隐藏此选项。,暫時顯示手動管理伺服器的密碼。由連結管理的伺服器會隱藏此選項。,手動管理サーバーのパスワードを一時的に表示します。リンク管理のサーバーではこのオプションは非表示です。,수동 관리 서버의 비밀번호를 일시적으로 표시합니다. 링크로 관리되는 서버에서는 이 옵션이 숨겨집니다.,Afficher temporairement le mot de passe des serveurs gérés manuellement. Cette option reste masquée pour les serveurs gérés par lien. +Try to register every configured global hotkey immediately.,Попытаться немедленно зарегистрировать все настроенные глобальные горячие клавиши.,立即尝试注册所有已配置的全局快捷键。,立即嘗試註冊所有已設定的全域快速鍵。,設定済みのグローバルホットキーをすべて今すぐ登録します。,구성된 모든 전역 단축키를 지금 등록해 봅니다.,Essayer d’enregistrer immédiatement tous les raccourcis globaux configurés. +URL downloaded and served when Online PAC mode is enabled.,"URL, который загружается и используется при включённом режиме Online PAC.",启用在线 PAC 模式时下载并提供的 URL。,啟用線上 PAC 模式時下載並提供的 URL。,オンライン PAC モード有効時にダウンロードして配信する URL です。,온라인 PAC 모드가 활성화되었을 때 다운로드하고 제공하는 URL입니다.,URL téléchargée et servie lorsque le mode PAC en ligne est activé. +Use the normal user-mode local proxy path without elevation or WinDivert capture.,Использовать обычный пользовательский режим локального прокси без повышения прав и перехвата WinDivert.,使用普通用户模式本地代理路径,不提升权限,也不进行 WinDivert 捕获。,使用一般使用者模式本機 Proxy 路徑,不提高權限,也不進行 WinDivert 擷取。,昇格や WinDivert キャプチャを使わず、通常のユーザーモードのローカルプロキシ経路を使用します。,권한 상승이나 WinDivert 캡처 없이 일반 사용자 모드 로컬 프록시 경로를 사용합니다.,"Utiliser le chemin proxy local normal en mode utilisateur, sans élévation ni capture WinDivert." +Validate and save all application routing rules.,Проверить и сохранить все правила маршрутизации приложений.,验证并保存所有应用程序路由规则。,驗證並儲存所有應用程式路由規則。,すべてのアプリケーションルーティングルールを検証して保存します。,모든 애플리케이션 라우팅 규칙을 검증하고 저장합니다.,Valider et enregistrer toutes les règles de routage d’application. +Validate and save the current server and local client settings.,Проверить и сохранить текущий сервер и настройки локального клиента.,验证并保存当前服务器和本地客户端设置。,驗證並儲存目前伺服器與本機用戶端設定。,現在のサーバーとローカルクライアント設定を検証して保存します。,현재 서버 및 로컬 클라이언트 설정을 검증하고 저장합니다.,Valider et enregistrer les paramètres actuels du serveur et du client local. +Validate and save the forward-proxy configuration.,Проверить и сохранить настройки вышестоящего прокси.,验证并保存上游代理配置。,驗證並儲存上游 Proxy 設定。,上流プロキシ設定を検証して保存します。,업스트림 프록시 구성을 검증하고 저장합니다.,Valider et enregistrer la configuration du proxy amont. +"View logs, traffic history and logging options.","Просматривать журналы, историю трафика и параметры журналирования.",查看日志、流量历史和日志选项。,檢視記錄、流量歷程與記錄選項。,ログ、トラフィック履歴、ログ設定を表示します。,"로그, 트래픽 기록 및 로깅 옵션을 봅니다.","Afficher les journaux, l’historique du trafic et les options de journalisation." +View version information and configure update checks.,Просматривать информацию о версии и настраивать проверку обновлений.,查看版本信息并配置更新检查。,檢視版本資訊並設定更新檢查。,バージョン情報を表示し、更新確認を設定します。,버전 정보를 보고 업데이트 확인을 구성합니다.,Afficher les informations de version et configurer la vérification des mises à jour. +Wrap long log lines instead of requiring horizontal scrolling.,Переносить длинные строки журнала вместо горизонтальной прокрутки.,自动换行长日志行,而不是要求水平滚动。,自動換行較長的記錄行,而不是要求水平捲動。,長いログ行を折り返し、横スクロールを不要にします。,긴 로그 줄을 줄 바꿈하여 가로 스크롤이 필요 없게 합니다.,Renvoyer à la ligne les longues lignes de journal au lieu d’imposer un défilement horizontal. +Plugins,Плагины,插件,外掛程式,プラグイン,플러그인,Plugins +Plugin,Плагин,插件,外掛程式,プラグイン,플러그인,Plugin +None,Нет,无,無,なし,없음,Aucun +Add plugin,Добавить плагин,添加插件,新增外掛程式,プラグインを追加,플러그인 추가,Ajouter un plugin +Choose a plugin,Выберите плагин,选择插件,選擇外掛程式,プラグインを選択,플러그인 선택,Choisir un plugin +Install,Установить,安装,安裝,インストール,설치,Installer +Import ZIP/TAR.GZ,Импорт ZIP/TAR.GZ,导入 ZIP/TAR.GZ,匯入 ZIP/TAR.GZ,ZIP/TAR.GZ をインポート,ZIP/TAR.GZ 가져오기,Importer ZIP/TAR.GZ +Installed plugins,Установленные плагины,已安装插件,已安裝外掛程式,インストール済みプラグイン,설치된 플러그인,Plugins installés +No plugins installed.,Плагины не установлены.,未安装插件。,尚未安裝外掛程式。,プラグインはインストールされていません。,설치된 플러그인이 없습니다.,Aucun plugin installé. +Plugin removed.,Плагин удалён.,插件已删除。,外掛程式已移除。,プラグインを削除しました。,플러그인이 제거되었습니다.,Plugin supprimé. +Install and manage SIP003 plugins.,Установка и управление плагинами SIP003.,安装和管理 SIP003 插件。,安裝和管理 SIP003 外掛程式。,SIP003 プラグインをインストールして管理します。,SIP003 플러그인을 설치하고 관리합니다.,Installer et gérer les plugins SIP003. +Install SIP003 plugins from the built-in list or import a plugin ZIP or TAR.GZ package.,Установите SIP003-плагин из списка или импортируйте пакет плагина ZIP или TAR.GZ.,从内置列表安装 SIP003 插件,或导入 ZIP 或 TAR.GZ 插件包。,從內建清單安裝 SIP003 外掛程式,或匯入 ZIP 或 TAR.GZ 外掛套件。,組み込みリストから SIP003 プラグインをインストールするか、ZIP または TAR.GZ パッケージをインポートします。,기본 목록에서 SIP003 플러그인을 설치하거나 ZIP 또는 TAR.GZ 패키지를 가져옵니다.,Installez un plugin SIP003 depuis la liste intégrée ou importez un paquet ZIP ou TAR.GZ. +Optional SIP003 plugin installed on the Plugins page.,"Необязательный SIP003-плагин, установленный на странице «Плагины».",可选的 SIP003 插件,可在“插件”页面安装。,可選的 SIP003 外掛程式,可在「外掛程式」頁面安裝。,「プラグイン」ページでインストールした任意の SIP003 プラグイン。,플러그인 페이지에서 설치한 선택적 SIP003 플러그인입니다.,Plugin SIP003 facultatif installé depuis la page Plugins. +Choose a supported Windows x64 SIP003 plugin to install.,Выберите поддерживаемый SIP003-плагин для Windows x64.,选择要安装的受支持 Windows x64 SIP003 插件。,選擇要安裝的受支援 Windows x64 SIP003 外掛程式。,インストールする Windows x64 対応 SIP003 プラグインを選択します。,설치할 지원되는 Windows x64 SIP003 플러그인을 선택합니다.,Choisissez un plugin SIP003 Windows x64 pris en charge. +Download the latest Windows x64 release archive and install the selected plugin.,Скачать последний архив Windows x64 и установить выбранный плагин.,下载最新的 Windows x64 版本归档并安装所选插件。,下載最新的 Windows x64 版本封存檔並安裝所選外掛程式。,最新の Windows x64 リリースアーカイブをダウンロードして選択したプラグインをインストールします。,최신 Windows x64 릴리스 아카이브를 다운로드하고 선택한 플러그인을 설치합니다.,Télécharger la dernière archive Windows x64 et installer le plugin sélectionné. +Import a local ZIP or TAR.GZ package containing a Windows plugin executable.,Импортировать локальный пакет ZIP или TAR.GZ с исполняемым файлом плагина Windows.,导入包含 Windows 插件可执行文件的本地 ZIP 或 TAR.GZ 包。,匯入包含 Windows 外掛程式執行檔的本機 ZIP 或 TAR.GZ 套件。,Windows プラグイン実行ファイルを含むローカル ZIP または TAR.GZ パッケージをインポートします。,Windows 플러그인 실행 파일이 포함된 로컬 ZIP 또는 TAR.GZ 패키지를 가져옵니다.,Importer un paquet ZIP ou TAR.GZ local contenant un exécutable de plugin Windows. +Remove this installed plugin from Shadowsocks storage.,Удалить установленный плагин из хранилища Shadowsocks.,从 Shadowsocks 存储中删除此已安装插件。,從 Shadowsocks 儲存空間中移除此已安裝外掛程式。,インストール済みプラグインを Shadowsocks ストレージから削除します。,설치된 플러그인을 Shadowsocks 저장소에서 제거합니다.,Supprimer ce plugin installé du stockage Shadowsocks. +Install SIP003 plugins from the built-in list or import a plugin ZIP or TAR.GZ package. Built-in catalog plugins can update automatically.,Устанавливайте SIP003-плагины из встроенного списка или импортируйте пакет ZIP/TAR.GZ. Плагины из встроенного каталога могут обновляться автоматически.,从内置列表安装 SIP003 插件,或导入 ZIP/TAR.GZ 插件包。内置目录中的插件可自动更新。,從內建清單安裝 SIP003 外掛程式,或匯入 ZIP/TAR.GZ 套件。內建目錄中的外掛程式可自動更新。,組み込みリストから SIP003 プラグインをインストールするか、ZIP/TAR.GZ パッケージをインポートします。組み込みカタログのプラグインは自動更新できます。,기본 목록에서 SIP003 플러그인을 설치하거나 ZIP/TAR.GZ 패키지를 가져옵니다. 기본 카탈로그 플러그인은 자동으로 업데이트할 수 있습니다.,Installez des plugins SIP003 depuis la liste intégrée ou importez un paquet ZIP/TAR.GZ. Les plugins du catalogue intégré peuvent se mettre à jour automatiquement. +Check updates,Проверить обновления,检查更新,檢查更新,更新を確認,업데이트 확인,Rechercher des mises à jour +Check installed catalog plugins for newer GitHub releases now.,Сейчас проверить установленные плагины из каталога на наличие новых релизов GitHub.,立即检查已安装的目录插件是否有较新的 GitHub 版本。,立即檢查已安裝的目錄外掛程式是否有較新的 GitHub 版本。,インストール済みのカタログプラグインについて、新しい GitHub リリースを今すぐ確認します。,설치된 카탈로그 플러그인의 최신 GitHub 릴리스를 지금 확인합니다.,Vérifier maintenant si de nouvelles versions GitHub existent pour les plugins du catalogue installés. +Built-in catalog plugins are checked automatically once per day when they are not in use.,"Плагины из встроенного каталога автоматически проверяются раз в сутки, когда они не используются.",内置目录插件在未使用时每天自动检查一次更新。,內建目錄外掛程式在未使用時每天自動檢查一次更新。,組み込みカタログのプラグインは、使用中でないときに1日1回自動確認されます。,기본 카탈로그 플러그인은 사용 중이 아닐 때 하루에 한 번 자동으로 확인됩니다.,Les plugins du catalogue intégré sont vérifiés automatiquement une fois par jour lorsqu’ils ne sont pas utilisés. +Release: {0},Релиз: {0},版本:{0},版本:{0},リリース: {0},릴리스: {0},Version : {0} +Automatic updates,Автоматические обновления,自动更新,自動更新,自動更新,자동 업데이트,Mises à jour automatiques +Automatically check this catalog plugin for a newer release once per day.,Автоматически проверять этот плагин из каталога на новый релиз раз в сутки.,每天自动检查一次此目录插件是否有新版本。,每天自動檢查一次此目錄外掛程式是否有新版本。,このカタログプラグインの新しいリリースを1日1回自動確認します。,이 카탈로그 플러그인의 새 릴리스를 하루에 한 번 자동으로 확인합니다.,Vérifier automatiquement une fois par jour si une nouvelle version de ce plugin du catalogue est disponible. +{0} plugin(s) updated.,Обновлено плагинов: {0}.,已更新 {0} 个插件。,已更新 {0} 個外掛程式。,{0} 個のプラグインを更新しました。,플러그인 {0}개를 업데이트했습니다.,Plugins mis à jour : {0}. +Plugin updates were deferred because one or more plugins are in use.,Обновление плагинов отложено: один или несколько плагинов сейчас используются.,插件更新已推迟,因为一个或多个插件正在使用中。,外掛程式更新已延後,因為一個或多個外掛程式正在使用中。,1つ以上のプラグインが使用中のため、更新を延期しました。,하나 이상의 플러그인이 사용 중이어서 업데이트가 연기되었습니다.,Les mises à jour des plugins ont été reportées car un ou plusieurs plugins sont en cours d’utilisation. +Installed catalog plugins are up to date.,Установленные плагины из каталога уже обновлены.,已安装的目录插件均为最新版本。,已安裝的目錄外掛程式均為最新版本。,インストール済みのカタログプラグインは最新です。,설치된 카탈로그 플러그인이 최신 상태입니다.,Les plugins du catalogue installés sont à jour. +{0} installed.,{0} установлен.,{0} 已安装。,{0} 已安裝。,{0} をインストールしました。,{0} 설치됨.,{0} installé. +DNS,DNS,DNS,DNS,DNS,DNS,DNS +Manage DNS policy and DNSCrypt Proxy.,Управление политикой DNS и DNSCrypt Proxy.,管理 DNS 策略和 DNSCrypt Proxy。,管理 DNS 原則與 DNSCrypt Proxy。,DNS ポリシーと DNSCrypt Proxy を管理します。,DNS 정책 및 DNSCrypt Proxy를 관리합니다.,Gérer la stratégie DNS et DNSCrypt Proxy. +Manage DNS policy and the optional DNSCrypt Proxy component downloaded from the official upstream release.,"Управляйте политикой DNS и дополнительным компонентом DNSCrypt Proxy, загружаемым из официального upstream-релиза.",管理 DNS 策略以及从官方上游版本下载的可选 DNSCrypt Proxy 组件。,管理 DNS 原則以及從官方上游版本下載的選用 DNSCrypt Proxy 元件。,DNS ポリシーと、公式 upstream リリースからダウンロードする任意の DNSCrypt Proxy コンポーネントを管理します。,DNS 정책과 공식 업스트림 릴리스에서 다운로드하는 선택적 DNSCrypt Proxy 구성 요소를 관리합니다.,Gérer la stratégie DNS et le composant DNSCrypt Proxy facultatif téléchargé depuis la version amont officielle. +DNS mode,Режим DNS,DNS 模式,DNS 模式,DNS モード,DNS 모드,Mode DNS +System DNS,Системный DNS,系统 DNS,系統 DNS,システム DNS,시스템 DNS,DNS système +Direct DNS,Прямой DNS,直连 DNS,直接 DNS,直接 DNS,직접 DNS,DNS direct +DNS through Shadowsocks,DNS через Shadowsocks,通过 Shadowsocks 的 DNS,透過 Shadowsocks 的 DNS,Shadowsocks 経由の DNS,Shadowsocks를 통한 DNS,DNS via Shadowsocks +Custom DoH,Пользовательский DoH,自定义 DoH,自訂 DoH,カスタム DoH,사용자 지정 DoH,DoH personnalisé +DNSCrypt,DNSCrypt,DNSCrypt,DNSCrypt,DNSCrypt,DNSCrypt,DNSCrypt +Administrator Mode transparently redirects captured UDP/TCP DNS traffic to DNSCrypt. User and Game modes do not provide system-wide interception.,Режим администратора прозрачно перенаправляет перехваченный UDP/TCP DNS-трафик в DNSCrypt. Пользовательский и игровой режимы не обеспечивают системный перехват.,管理员模式会将捕获的 UDP/TCP DNS 流量透明重定向到 DNSCrypt。用户模式和游戏模式不提供全系统拦截。,系統管理員模式會將擷取的 UDP/TCP DNS 流量透明重新導向到 DNSCrypt。使用者模式和遊戲模式不提供全系統攔截。,管理者モードでは、キャプチャした UDP/TCP DNS トラフィックを DNSCrypt に透過的にリダイレクトします。ユーザーモードとゲームモードではシステム全体のインターセプトは行いません。,관리자 모드는 캡처된 UDP/TCP DNS 트래픽을 DNSCrypt로 투명하게 리디렉션합니다. 사용자 및 게임 모드는 시스템 전체 가로채기를 제공하지 않습니다.,Le mode administrateur redirige de façon transparente le trafic DNS UDP/TCP capturé vers DNSCrypt. Les modes Utilisateur et Jeu n’assurent pas d’interception à l’échelle du système. +DNSCrypt Proxy,DNSCrypt Proxy,DNSCrypt Proxy,DNSCrypt Proxy,DNSCrypt Proxy,DNSCrypt Proxy,DNSCrypt Proxy +Status,Состояние,状态,狀態,状態,상태,État +Installed version,Установленная версия,已安装版本,已安裝版本,インストール済みバージョン,설치된 버전,Version installée +Latest version,Последняя версия,最新版本,最新版本,最新バージョン,최신 버전,Dernière version +Local port,Локальный порт,本地端口,本機連接埠,ローカルポート,로컬 포트,Port local +Coverage,Охват,覆盖范围,涵蓋範圍,適用範囲,적용 범위,Couverture +Not installed,Не установлен,未安装,未安裝,未インストール,설치되지 않음,Non installé +Full DNS interception requires Administrator Mode.,Полный перехват DNS требует режима администратора.,完整 DNS 拦截需要管理员模式。,完整 DNS 攔截需要系統管理員模式。,完全な DNS インターセプトには管理者モードが必要です。,전체 DNS 가로채기에는 관리자 모드가 필요합니다.,L’interception DNS complète nécessite le mode administrateur. +System DNS is unchanged.,Системный DNS не изменён.,系统 DNS 未更改。,系統 DNS 未變更。,システム DNS は変更されていません。,시스템 DNS는 변경되지 않았습니다.,Le DNS système reste inchangé. +Transparent DNS interception is active for captured applications.,Прозрачный перехват DNS активен для перехваченных приложений.,已对捕获的应用启用透明 DNS 拦截。,已對擷取的應用程式啟用透明 DNS 攔截。,キャプチャ対象アプリケーションで透過 DNS インターセプトが有効です。,캡처된 애플리케이션에 투명 DNS 가로채기가 활성화되었습니다.,L’interception DNS transparente est active pour les applications capturées. +DNS is blocked (fail-closed) because DNSCrypt is unavailable.,"DNS заблокирован (fail-closed), поскольку DNSCrypt недоступен.",由于 DNSCrypt 不可用,DNS 已按故障关闭策略阻止。,由於 DNSCrypt 不可用,DNS 已依故障關閉策略封鎖。,DNSCrypt が利用できないため、DNS は fail-closed でブロックされています.,DNSCrypt를 사용할 수 없어 DNS가 fail-closed 방식으로 차단되었습니다.,Le DNS est bloqué (fail-closed) car DNSCrypt est indisponible. +"DNSCrypt is running, but system-wide interception is paused in Game Mode.","DNSCrypt работает, но системный перехват приостановлен в игровом режиме.",DNSCrypt 正在运行,但游戏模式下全系统拦截已暂停。,DNSCrypt 正在執行,但遊戲模式下全系統攔截已暫停。,DNSCrypt は動作中ですが、ゲームモードではシステム全体のインターセプトが一時停止されています。,DNSCrypt는 실행 중이지만 게임 모드에서는 시스템 전체 가로채기가 일시 중지됩니다.,"DNSCrypt fonctionne, mais l’interception à l’échelle du système est suspendue en mode Jeu." +DNSCrypt is running; full DNS interception requires Administrator Mode.,DNSCrypt работает; полный перехват DNS требует режима администратора.,DNSCrypt 正在运行;完整 DNS 拦截需要管理员模式。,DNSCrypt 正在執行;完整 DNS 攔截需要系統管理員模式。,DNSCrypt は動作中です。完全な DNS インターセプトには管理者モードが必要です。,DNSCrypt가 실행 중입니다. 전체 DNS 가로채기에는 관리자 모드가 필요합니다.,DNSCrypt fonctionne ; l’interception DNS complète nécessite le mode administrateur. +System-wide DNS interception is paused in Game Mode.,Системный перехват DNS приостановлен в игровом режиме.,游戏模式下全系统 DNS 拦截已暂停。,遊戲模式下全系統 DNS 攔截已暫停。,ゲームモードではシステム全体の DNS インターセプトが一時停止されています。,게임 모드에서는 시스템 전체 DNS 가로채기가 일시 중지됩니다.,L’interception DNS à l’échelle du système est suspendue en mode Jeu. +DNSCrypt is unavailable; full DNS interception requires Administrator Mode.,DNSCrypt недоступен; полный перехват DNS требует режима администратора.,DNSCrypt 不可用;完整 DNS 拦截需要管理员模式。,DNSCrypt 不可用;完整 DNS 攔截需要系統管理員模式。,DNSCrypt は利用できません。完全な DNS インターセプトには管理者モードが必要です。,DNSCrypt를 사용할 수 없습니다. 전체 DNS 가로채기에는 관리자 모드가 필요합니다.,DNSCrypt est indisponible ; l’interception DNS complète nécessite le mode administrateur. +Install DNSCrypt Proxy,Установить DNSCrypt Proxy,安装 DNSCrypt Proxy,安裝 DNSCrypt Proxy,DNSCrypt Proxy をインストール,DNSCrypt Proxy 설치,Installer DNSCrypt Proxy +Check for DNSCrypt updates,Проверить обновления DNSCrypt,检查 DNSCrypt 更新,檢查 DNSCrypt 更新,DNSCrypt の更新を確認,DNSCrypt 업데이트 확인,Vérifier les mises à jour DNSCrypt +Check DNSCrypt Update,Проверить обновление DNSCrypt,检查 DNSCrypt 更新,檢查 DNSCrypt 更新,DNSCrypt 更新を確認,DNSCrypt 업데이트 확인,Vérifier la mise à jour DNSCrypt +Restart,Перезапустить,重启,重新啟動,再起動,다시 시작,Redémarrer +Reinstall,Переустановить,重新安装,重新安裝,再インストール,재설치,Réinstaller +Remove DNSCrypt Proxy,Удалить DNSCrypt Proxy,删除 DNSCrypt Proxy,移除 DNSCrypt Proxy,DNSCrypt Proxy を削除,DNSCrypt Proxy 제거,Supprimer DNSCrypt Proxy +DNSCrypt settings,Настройки DNSCrypt,DNSCrypt 设置,DNSCrypt 設定,DNSCrypt 設定,DNSCrypt 설정,Paramètres DNSCrypt +Automatically update DNSCrypt Proxy,Автоматически обновлять DNSCrypt Proxy,自动更新 DNSCrypt Proxy,自動更新 DNSCrypt Proxy,DNSCrypt Proxy を自動更新,DNSCrypt Proxy 자동 업데이트,Mettre à jour automatiquement DNSCrypt Proxy +Require DNSSEC,Требовать DNSSEC,要求 DNSSEC,要求 DNSSEC,DNSSEC を必須にする,DNSSEC 필요,Exiger DNSSEC +Require no-log resolvers,Требовать резолверы без журналирования,要求无日志解析器,要求無記錄解析器,ログを保存しないリゾルバーを必須にする,무로그 리졸버 필요,Exiger des résolveurs sans journalisation +Require unfiltered resolvers,Требовать резолверы без фильтрации,要求未过滤解析器,要求未過濾解析器,フィルタリングしないリゾルバーを必須にする,필터링되지 않은 리졸버 필요,Exiger des résolveurs non filtrés +Use IPv6 resolvers,Использовать IPv6-резолверы,使用 IPv6 解析器,使用 IPv6 解析器,IPv6 リゾルバーを使用,IPv6 리졸버 사용,Utiliser les résolveurs IPv6 +Route DNSCrypt through Shadowsocks,Направлять DNSCrypt через Shadowsocks,通过 Shadowsocks 路由 DNSCrypt,透過 Shadowsocks 路由 DNSCrypt,DNSCrypt を Shadowsocks 経由でルーティング,Shadowsocks를 통해 DNSCrypt 라우팅,Acheminer DNSCrypt via Shadowsocks +DNSCrypt operation,Операция DNSCrypt,DNSCrypt 操作,DNSCrypt 操作,DNSCrypt 操作,DNSCrypt 작업,Opération DNSCrypt +Ready,Готово,就绪,就緒,準備完了,준비됨,Prêt +Checking latest version,Проверка последней версии,正在检查最新版本,正在檢查最新版本,最新バージョンを確認中,최신 버전 확인 중,Vérification de la dernière version +Downloading DNSCrypt Proxy,Загрузка DNSCrypt Proxy,正在下载 DNSCrypt Proxy,正在下載 DNSCrypt Proxy,DNSCrypt Proxy をダウンロード中,DNSCrypt Proxy 다운로드 중,Téléchargement de DNSCrypt Proxy +Downloading signature,Загрузка подписи,正在下载签名,正在下載簽章,署名をダウンロード中,서명 다운로드 중,Téléchargement de la signature +Verifying signature,Проверка подписи,正在验证签名,正在驗證簽章,署名を検証中,서명 검증 중,Vérification de la signature +Installing,Установка,正在安装,正在安裝,インストール中,설치 중,Installation +Prepared,Подготовлено,已准备,已準備,準備済み,준비됨,Préparé +Validating DNSCrypt runtime,Проверка runtime DNSCrypt,正在验证 DNSCrypt 运行时,正在驗證 DNSCrypt 執行階段,DNSCrypt ランタイムを検証中,DNSCrypt 런타임 검증 중,Validation du runtime DNSCrypt +Activating,Активация,正在激活,正在啟用,有効化中,활성화 중,Activation +Starting DNSCrypt,Запуск DNSCrypt,正在启动 DNSCrypt,正在啟動 DNSCrypt,DNSCrypt を起動中,DNSCrypt 시작 중,Démarrage de DNSCrypt +Removing,Удаление,正在删除,正在移除,削除中,제거 중,Suppression +Starting,Запускается,正在启动,正在啟動,起動中,시작 중,Démarrage +Updating,Обновляется,正在更新,正在更新,更新中,업데이트 중,Mise à jour +Failed,Ошибка,失败,失敗,失敗,실패,Échec +DNSCrypt Proxy is not installed.,DNSCrypt Proxy не установлен.,DNSCrypt Proxy 未安装。,DNSCrypt Proxy 未安裝。,DNSCrypt Proxy はインストールされていません。,DNSCrypt Proxy가 설치되지 않았습니다.,DNSCrypt Proxy n’est pas installé. +Shadowsocks can download and verify the official Windows x64 release from DNSCrypt/dnscrypt-proxy.,Shadowsocks может скачать и проверить официальный релиз Windows x64 из DNSCrypt/dnscrypt-proxy.,Shadowsocks 可以从 DNSCrypt/dnscrypt-proxy 下载并验证官方 Windows x64 版本。,Shadowsocks 可以從 DNSCrypt/dnscrypt-proxy 下載並驗證官方 Windows x64 版本。,Shadowsocks は DNSCrypt/dnscrypt-proxy から公式 Windows x64 リリースをダウンロードして検証できます。,Shadowsocks는 DNSCrypt/dnscrypt-proxy에서 공식 Windows x64 릴리스를 다운로드하고 검증할 수 있습니다.,Shadowsocks peut télécharger et vérifier la version Windows x64 officielle depuis DNSCrypt/dnscrypt-proxy. +Download and enable,Скачать и включить,下载并启用,下載並啟用,ダウンロードして有効化,다운로드 및 활성화,Télécharger et activer +DNS mode will switch to System DNS and the downloaded DNSCrypt component will be removed.,"Режим DNS переключится на системный DNS, а загруженный компонент DNSCrypt будет удалён.",DNS 模式将切换为系统 DNS,并删除已下载的 DNSCrypt 组件。,DNS 模式將切換為系統 DNS,並移除已下載的 DNSCrypt 元件。,DNS モードをシステム DNS に切り替え、ダウンロード済み DNSCrypt コンポーネントを削除します。,DNS 모드가 시스템 DNS로 전환되고 다운로드한 DNSCrypt 구성 요소가 제거됩니다.,Le mode DNS passera au DNS système et le composant DNSCrypt téléchargé sera supprimé. +DNSCrypt operation failed. See Logs for details.,Операция DNSCrypt завершилась ошибкой. Подробности в журнале.,DNSCrypt 操作失败。详情请查看日志。,DNSCrypt 操作失敗。詳情請查看記錄。,DNSCrypt 操作に失敗しました。詳細はログを確認してください。,DNSCrypt 작업이 실패했습니다. 자세한 내용은 로그를 확인하세요.,L’opération DNSCrypt a échoué. Consultez les journaux pour plus de détails. +Operation cancelled.,Операция отменена.,操作已取消。,操作已取消。,操作をキャンセルしました。,작업이 취소되었습니다.,Opération annulée. +DNSCrypt Proxy installed.,DNSCrypt Proxy установлен.,DNSCrypt Proxy 已安装。,DNSCrypt Proxy 已安裝。,DNSCrypt Proxy をインストールしました。,DNSCrypt Proxy가 설치되었습니다.,DNSCrypt Proxy installé. +DNSCrypt Proxy updated.,DNSCrypt Proxy обновлён.,DNSCrypt Proxy 已更新。,DNSCrypt Proxy 已更新。,DNSCrypt Proxy を更新しました。,DNSCrypt Proxy가 업데이트되었습니다.,DNSCrypt Proxy mis à jour. +DNSCrypt Proxy restarted.,DNSCrypt Proxy перезапущен.,DNSCrypt Proxy 已重启。,DNSCrypt Proxy 已重新啟動。,DNSCrypt Proxy を再起動しました。,DNSCrypt Proxy가 다시 시작되었습니다.,DNSCrypt Proxy redémarré. +DNSCrypt Proxy reinstalled.,DNSCrypt Proxy переустановлен.,DNSCrypt Proxy 已重新安装。,DNSCrypt Proxy 已重新安裝。,DNSCrypt Proxy を再インストールしました。,DNSCrypt Proxy가 재설치되었습니다.,DNSCrypt Proxy réinstallé. +DNSCrypt Proxy removed.,DNSCrypt Proxy удалён.,DNSCrypt Proxy 已删除。,DNSCrypt Proxy 已移除。,DNSCrypt Proxy を削除しました。,DNSCrypt Proxy가 제거되었습니다.,DNSCrypt Proxy supprimé. +System DNS enabled.,Системный DNS включён.,已启用系统 DNS。,已啟用系統 DNS。,システム DNS を有効にしました。,시스템 DNS가 활성화되었습니다.,DNS système activé. +DNSCrypt mode selected; runtime is running.,Выбран режим DNSCrypt; runtime запущен.,已选择 DNSCrypt 模式;运行时正在运行。,已選擇 DNSCrypt 模式;執行階段正在執行。,DNSCrypt モードを選択し、ランタイムを起動しました。,DNSCrypt 모드가 선택되었고 런타임이 실행 중입니다.,Mode DNSCrypt sélectionné ; le runtime est actif. +DNSCrypt Proxy is up to date.,DNSCrypt Proxy уже обновлён до последней версии.,DNSCrypt Proxy 已是最新版本。,DNSCrypt Proxy 已是最新版本。,DNSCrypt Proxy は最新です。,DNSCrypt Proxy가 최신 상태입니다.,DNSCrypt Proxy est à jour. +DNSCrypt Proxy {0} is available.,Доступна версия DNSCrypt Proxy {0}.,DNSCrypt Proxy {0} 可用。,DNSCrypt Proxy {0} 可用。,DNSCrypt Proxy {0} が利用可能です。,DNSCrypt Proxy {0}을 사용할 수 있습니다.,DNSCrypt Proxy {0} est disponible. +Failed: {0},Ошибка: {0},失败:{0},失敗:{0},失敗: {0},실패: {0},Échec : {0} +DNS Settings,Настройки DNS,DNS 设置,DNS 設定,DNS 設定,DNS 설정,Paramètres DNS +DNSCrypt: Not installed,DNSCrypt: не установлен,DNSCrypt:未安装,DNSCrypt:未安裝,DNSCrypt: 未インストール,DNSCrypt: 설치되지 않음,DNSCrypt : non installé +DNSCrypt: Running · {0},DNSCrypt: работает · {0},DNSCrypt:运行中 · {0},DNSCrypt:執行中 · {0},DNSCrypt: 実行中 · {0},DNSCrypt: 실행 중 · {0},DNSCrypt : actif · {0} +DNSCrypt: Stopped · {0},DNSCrypt: остановлен · {0},DNSCrypt:已停止 · {0},DNSCrypt:已停止 · {0},DNSCrypt: 停止 · {0},DNSCrypt: 중지됨 · {0},DNSCrypt : arrêté · {0} +DNSCrypt: Update available · {0},DNSCrypt: доступно обновление · {0},DNSCrypt:有可用更新 · {0},DNSCrypt:有可用更新 · {0},DNSCrypt: 更新あり · {0},DNSCrypt: 업데이트 가능 · {0},DNSCrypt : mise à jour disponible · {0} +DNSCrypt: Failed,DNSCrypt: ошибка,DNSCrypt:失败,DNSCrypt:失敗,DNSCrypt: 失敗,DNSCrypt: 실패,DNSCrypt : échec +Resolver selection,Выбор резолверов,解析器选择,解析器選擇,リゾルバーの選択,리졸버 선택,Sélection des résolveurs +Automatic,Автоматически,自动,自動,自動,자동,Automatique +Selected servers,Выбранные серверы,所选服务器,選取的伺服器,選択したサーバー,선택한 서버,Serveurs sélectionnés +DNSCrypt: Starting…,DNSCrypt: запускается…,DNSCrypt:正在启动…,DNSCrypt:正在啟動…,DNSCrypt: 起動中…,DNSCrypt: 시작 중…,DNSCrypt : démarrage… +DNSCrypt: Updating…,DNSCrypt: обновляется…,DNSCrypt:正在更新…,DNSCrypt:正在更新…,DNSCrypt: 更新中…,DNSCrypt: 업데이트 중…,DNSCrypt : mise à jour… +Available resolvers,Доступные резолверы,可用解析器,可用解析器,利用可能なリゾルバー,사용 가능한 리졸버,Résolveurs disponibles +Load resolver list,Загрузить список резолверов,加载解析器列表,載入解析器清單,リゾルバー一覧を読み込む,리졸버 목록 불러오기,Charger la liste des résolveurs +Resolver list is loaded from DNSCrypt Proxy's signed upstream catalog.,Список резолверов загружается из подписанного upstream-каталога DNSCrypt Proxy.,解析器列表从 DNSCrypt Proxy 的签名上游目录加载。,解析器清單從 DNSCrypt Proxy 的簽名上游目錄載入。,リゾルバー一覧は DNSCrypt Proxy の署名済み upstream カタログから読み込まれます.,리졸버 목록은 DNSCrypt Proxy의 서명된 업스트림 카탈로그에서 불러옵니다.,La liste des résolveurs est chargée depuis le catalogue amont signé de DNSCrypt Proxy. +Loading resolver list,Загрузка списка резолверов,正在加载解析器列表,正在載入解析器清單,リゾルバー一覧を読み込み中,리졸버 목록 불러오는 중,Chargement de la liste des résolveurs +Resolver list loaded.,Список резолверов загружен.,解析器列表已加载。,解析器清單已載入。,リゾルバー一覧を読み込みました。,리졸버 목록을 불러왔습니다.,Liste des résolveurs chargée. +Loaded {0} upstream resolvers.,Загружено резолверов из upstream: {0}.,已加载 {0} 个上游解析器。,已載入 {0} 個上游解析器。,upstream リゾルバーを {0} 件読み込みました。,업스트림 리졸버 {0}개를 불러왔습니다.,{0} résolveurs amont chargés. +Select at least one resolver.,Выберите хотя бы один резолвер.,请至少选择一个解析器。,請至少選擇一個解析器。,少なくとも 1 つのリゾルバーを選択してください。,리졸버를 하나 이상 선택하세요.,Sélectionnez au moins un résolveur. +DNSCrypt is configured and will start when Administrator Mode is enabled.,DNSCrypt настроен и будет запущен при включении режима администратора.,DNSCrypt 已配置,并将在启用管理员模式后启动。,DNSCrypt 已設定,並會在啟用系統管理員模式後啟動。,DNSCrypt は構成済みで、管理者モードを有効にすると起動します。,DNSCrypt가 구성되었으며 관리자 모드를 사용하면 시작됩니다.,DNSCrypt est configuré et démarrera lorsque le mode administrateur sera activé. +Automatic DNSCrypt update failed: {0},Автоматическое обновление DNSCrypt завершилось ошибкой: {0},DNSCrypt 自动更新失败:{0},DNSCrypt 自動更新失敗:{0},DNSCrypt の自動更新に失敗しました: {0},DNSCrypt 자동 업데이트 실패: {0},Échec de la mise à jour automatique de DNSCrypt : {0} +When DNSCrypt is routed through Shadowsocks the Shadowsocks server and forward-proxy endpoints must use IP addresses to prevent DNS bootstrap recursion.,При маршрутизации DNSCrypt через Shadowsocks серверы Shadowsocks и forward proxy должны использовать IP-адреса для предотвращения рекурсивного DNS bootstrap.,通过 Shadowsocks 路由 DNSCrypt 时 Shadowsocks 服务器和转发代理端点必须使用 IP 地址以防止 DNS 引导递归。,透過 Shadowsocks 路由 DNSCrypt 時 Shadowsocks 伺服器與轉送 Proxy 端點必須使用 IP 位址以避免 DNS 啟動遞迴。,DNSCrypt を Shadowsocks 経由でルーティングする場合 DNS ブートストラップの再帰を防ぐため Shadowsocks サーバーとフォワードプロキシのエンドポイントは IP アドレスを使用する必要があります。,DNSCrypt를 Shadowsocks를 통해 라우팅할 때 DNS 부트스트랩 재귀를 방지하려면 Shadowsocks 서버 및 전달 프록시 엔드포인트가 IP 주소를 사용해야 합니다.,Lorsque DNSCrypt est routé via Shadowsocks les points de terminaison du serveur Shadowsocks et du proxy de transfert doivent utiliser des adresses IP afin d’éviter une récursion d’amorçage DNS. +Third-party notices,Уведомления сторонних компонентов,第三方声明,第三方聲明,サードパーティ通知,서드파티 고지,Mentions de tiers +Show third-party license and source notices bundled with this application.,Показать лицензии и сведения об исходном коде сторонних компонентов в составе приложения.,显示随此应用程序提供的第三方许可证和源代码声明。,顯示此應用程式隨附的第三方授權與原始碼聲明。,このアプリケーションに含まれるサードパーティのライセンスとソース通知を表示します。,이 애플리케이션에 포함된 서드파티 라이선스 및 소스 고지를 표시합니다.,Afficher les licences tierces et les informations de source incluses dans cette application. +Third-party notices resource is unavailable.,Ресурс уведомлений сторонних компонентов недоступен.,第三方声明资源不可用。,第三方聲明資源無法使用。,サードパーティ通知リソースを利用できません。,서드파티 고지 리소스를 사용할 수 없습니다.,La ressource des mentions de tiers est indisponible. +License,Лицензия,许可证,授權,ライセンス,라이선스,Licence +Show the Shadowsocks Reborn license bundled with this application.,"Показать лицензию Shadowsocks Reborn, включённую в состав этого приложения.",显示此应用程序随附的 Shadowsocks Reborn 许可证。,顯示此應用程式隨附的 Shadowsocks Reborn 授權。,このアプリケーションに同梱されている Shadowsocks Reborn のライセンスを表示します。,이 애플리케이션에 포함된 Shadowsocks Reborn 라이선스를 표시합니다.,Afficher la licence Shadowsocks Reborn intégrée à cette application. +License resource is unavailable.,Ресурс лицензии недоступен.,许可证资源不可用。,授權資源無法使用。,ライセンス リソースを利用できません。,라이선스 리소스를 사용할 수 없습니다.,La ressource de licence est indisponible. +Custom DoH URL,URL пользовательского DoH,自定义 DoH URL,自訂 DoH URL,カスタム DoH URL,사용자 지정 DoH URL,URL DoH personnalisée +"Administrator Mode transparently applies DNS through Shadowsocks, Custom DoH and DNSCrypt to captured UDP/TCP DNS traffic. In User Mode DNSCrypt still runs for Shadowsocks-managed DNS, but system-wide interception is unavailable.","Режим администратора прозрачно применяет DNS через Shadowsocks, пользовательский DoH и DNSCrypt к перехваченному UDP/TCP DNS-трафику. В пользовательском режиме DNSCrypt продолжает работать для DNS, управляемого Shadowsocks, но системный перехват недоступен.",管理员模式会将通过 Shadowsocks 的 DNS、自定义 DoH 和 DNSCrypt 透明应用于捕获的 UDP/TCP DNS 流量。在用户模式下,DNSCrypt 仍用于 Shadowsocks 管理的 DNS,但无法进行全系统拦截。,系統管理員模式會將透過 Shadowsocks 的 DNS、自訂 DoH 與 DNSCrypt 透明套用至擷取的 UDP/TCP DNS 流量。在使用者模式下,DNSCrypt 仍用於 Shadowsocks 管理的 DNS,但無法進行全系統攔截。,管理者モードでは、Shadowsocks 経由 DNS、カスタム DoH、DNSCrypt をキャプチャした UDP/TCP DNS トラフィックに透過的に適用します。ユーザーモードでも Shadowsocks が管理する DNS 用に DNSCrypt は動作しますが、システム全体のインターセプトは利用できません。,"관리자 모드는 Shadowsocks 경유 DNS, 사용자 지정 DoH 및 DNSCrypt를 캡처된 UDP/TCP DNS 트래픽에 투명하게 적용합니다. 사용자 모드에서도 Shadowsocks가 관리하는 DNS에 DNSCrypt가 실행되지만 시스템 전체 가로채기는 사용할 수 없습니다.","Le mode administrateur applique de façon transparente le DNS via Shadowsocks, le DoH personnalisé et DNSCrypt au trafic DNS UDP/TCP capturé. En mode Utilisateur, DNSCrypt continue de fonctionner pour le DNS géré par Shadowsocks, mais l’interception globale du système est indisponible." +DNS is forced direct and bypasses Shadowsocks routing.,DNS принудительно направляется напрямую в обход маршрутизации Shadowsocks.,DNS 被强制直连并绕过 Shadowsocks 路由。,DNS 會強制直接連線並繞過 Shadowsocks 路由。,DNS は強制的に直接接続され、Shadowsocks ルーティングをバイパスします。,DNS는 강제로 직접 연결되며 Shadowsocks 라우팅을 우회합니다.,Le DNS est forcé en accès direct et contourne le routage Shadowsocks. +DNS traffic is routed through Shadowsocks for captured applications.,DNS-трафик перехваченных приложений маршрутизируется через Shadowsocks.,捕获应用的 DNS 流量通过 Shadowsocks 路由。,擷取應用程式的 DNS 流量會透過 Shadowsocks 路由。,キャプチャ対象アプリケーションの DNS トラフィックは Shadowsocks 経由でルーティングされます。,캡처된 애플리케이션의 DNS 트래픽이 Shadowsocks를 통해 라우팅됩니다.,Le trafic DNS des applications capturées est acheminé via Shadowsocks. +DNS through Shadowsocks requires Administrator Mode for transparent system-wide interception.,Для прозрачного системного перехвата DNS через Shadowsocks требуется режим администратора.,通过 Shadowsocks 的 DNS 需要管理员模式才能进行透明的全系统拦截。,透過 Shadowsocks 的 DNS 需要系統管理員模式才能進行透明的全系統攔截。,Shadowsocks 経由 DNS の透過的なシステム全体インターセプトには管理者モードが必要です。,Shadowsocks 경유 DNS의 투명한 시스템 전체 가로채기에는 관리자 모드가 필요합니다.,Le DNS via Shadowsocks nécessite le mode administrateur pour une interception transparente à l’échelle du système. +Custom DoH is active for captured applications.,Пользовательский DoH активен для перехваченных приложений.,已对捕获的应用启用自定义 DoH。,已對擷取的應用程式啟用自訂 DoH。,キャプチャ対象アプリケーションでカスタム DoH が有効です。,캡처된 애플리케이션에 사용자 지정 DoH가 활성화되었습니다.,Le DoH personnalisé est actif pour les applications capturées. +DNS is blocked because the Custom DoH endpoint is unavailable or invalid.,"DNS заблокирован, поскольку адрес пользовательского DoH недоступен или некорректен.",由于自定义 DoH 端点不可用或无效,DNS 已被阻止。,由於自訂 DoH 端點不可用或無效,DNS 已被封鎖。,カスタム DoH エンドポイントが利用できないか無効なため、DNS はブロックされています。,사용자 지정 DoH 엔드포인트를 사용할 수 없거나 올바르지 않아 DNS가 차단되었습니다.,Le DNS est bloqué car le point de terminaison DoH personnalisé est indisponible ou invalide. +Custom DoH requires Administrator Mode for transparent system-wide interception.,Для прозрачного системного перехвата пользовательского DoH требуется режим администратора.,自定义 DoH 需要管理员模式才能进行透明的全系统拦截。,自訂 DoH 需要系統管理員模式才能進行透明的全系統攔截。,カスタム DoH の透過的なシステム全体インターセプトには管理者モードが必要です。,사용자 지정 DoH의 투명한 시스템 전체 가로채기에는 관리자 모드가 필요합니다.,Le DoH personnalisé nécessite le mode administrateur pour une interception transparente à l’échelle du système. +DNSCrypt is running in User Mode; system-wide interception requires Administrator Mode.,DNSCrypt работает в пользовательском режиме; для системного перехвата требуется режим администратора.,DNSCrypt 正在用户模式下运行;全系统拦截需要管理员模式。,DNSCrypt 正在使用者模式下執行;全系統攔截需要系統管理員模式。,DNSCrypt はユーザーモードで動作中です。システム全体のインターセプトには管理者モードが必要です。,DNSCrypt가 사용자 모드에서 실행 중입니다. 시스템 전체 가로채기에는 관리자 모드가 필요합니다.,DNSCrypt fonctionne en mode Utilisateur ; l’interception à l’échelle du système nécessite le mode administrateur. +DNSCrypt is selected but its runtime is unavailable.,"DNSCrypt выбран, но его runtime недоступен.",已选择 DNSCrypt,但其运行时不可用。,已選擇 DNSCrypt,但其執行階段不可用。,DNSCrypt が選択されていますが、ランタイムを利用できません。,DNSCrypt가 선택되었지만 런타임을 사용할 수 없습니다.,"DNSCrypt est sélectionné, mais son runtime est indisponible." +Direct DNS enabled.,Прямой DNS включён.,已启用直连 DNS。,已啟用直接 DNS。,直接 DNS を有効にしました。,직접 DNS가 활성화되었습니다.,DNS direct activé. +DNS through Shadowsocks enabled.,DNS через Shadowsocks включён.,已启用通过 Shadowsocks 的 DNS。,已啟用透過 Shadowsocks 的 DNS。,Shadowsocks 経由 DNS を有効にしました。,Shadowsocks 경유 DNS가 활성화되었습니다.,DNS via Shadowsocks activé. +Custom DoH enabled.,Пользовательский DoH включён.,已启用自定义 DoH。,已啟用自訂 DoH。,カスタム DoH を有効にしました。,사용자 지정 DoH가 활성화되었습니다.,DoH personnalisé activé. +Direct DNS is selected; User Mode does not intercept system DNS.,Выбран прямой DNS; в пользовательском режиме системный DNS не перехватывается.,已选择直连 DNS;用户模式不会拦截系统 DNS。,已選擇直接 DNS;使用者模式不會攔截系統 DNS。,直接 DNS が選択されています。ユーザーモードではシステム DNS はインターセプトされません。,직접 DNS가 선택되었습니다. 사용자 모드에서는 시스템 DNS를 가로채지 않습니다.,Le DNS direct est sélectionné ; le mode Utilisateur n’intercepte pas le DNS système. +Test DNSCrypt,Проверить DNSCrypt,测试 DNSCrypt,測試 DNSCrypt,DNSCrypt をテスト,DNSCrypt 테스트,Tester DNSCrypt +Testing DNSCrypt,Проверка DNSCrypt,正在测试 DNSCrypt,正在測試 DNSCrypt,DNSCrypt をテスト中,DNSCrypt 테스트 중,Test de DNSCrypt +DNSCrypt test passed.,Проверка DNSCrypt пройдена.,DNSCrypt 测试通过。,DNSCrypt 測試通過。,DNSCrypt テストに成功しました。,DNSCrypt 테스트를 통과했습니다.,Test DNSCrypt réussi. +DNSCrypt test failed.,Проверка DNSCrypt не пройдена.,DNSCrypt 测试失败。,DNSCrypt 測試失敗。,DNSCrypt テストに失敗しました。,DNSCrypt 테스트에 실패했습니다.,Échec du test DNSCrypt. +Manual selection,Ручной выбор,手动选择,手動選擇,手動選択,수동 선택,Sélection manuelle +Automatically selected resolvers,Автоматически выбранные резолверы,自动选择的解析器,自動選擇的解析器,自動選択されたリゾルバー,자동 선택된 리졸버,Résolveurs sélectionnés automatiquement +Search resolvers,Поиск резолверов,搜索解析器,搜尋解析器,リゾルバーを検索,리졸버 검색,Rechercher des résolveurs +Showing {0} matching resolver(s).,Показано подходящих резолверов: {0}.,显示 {0} 个匹配的解析器。,顯示 {0} 個相符的解析器。,一致するリゾルバーを {0} 件表示しています。,일치하는 리졸버 {0}개를 표시합니다.,{0} résolveur(s) correspondant(s) affiché(s). +DNS settings are saved and applied automatically.,Настройки DNS сохраняются и применяются автоматически.,DNS 设置会自动保存并应用。,DNS 設定會自動儲存並套用。,DNS 設定は自動的に保存され適用されます。,DNS 설정은 자동으로 저장되고 적용됩니다.,Les paramètres DNS sont enregistrés et appliqués automatiquement. +DNS settings applied.,Настройки DNS применены.,DNS 设置已应用。,DNS 設定已套用。,DNS 設定を適用しました。,DNS 설정이 적용되었습니다.,Paramètres DNS appliqués. +DNS status,Состояние DNS,DNS 状态,DNS 狀態,DNS 状態,DNS 상태,État DNS +Active adapters,Активные адаптеры,活动适配器,作用中的介面卡,アクティブなアダプター,활성 어댑터,Adaptateurs actifs +Windows DNS servers,DNS-серверы Windows,Windows DNS 服务器,Windows DNS 伺服器,Windows DNS サーバー,Windows DNS 서버,Serveurs DNS Windows +Shadowsocks DNS policy,Политика DNS Shadowsocks,Shadowsocks DNS 策略,Shadowsocks DNS 原則,Shadowsocks DNS ポリシー,Shadowsocks DNS 정책,Stratégie DNS Shadowsocks +Transparent interception,Прозрачный перехват,透明拦截,透明攔截,透過インターセプト,투명 가로채기,Interception transparente +System DNS information is read from active Windows network adapters; Shadowsocks does not change adapter DNS addresses.,Сведения о системном DNS считываются с активных сетевых адаптеров Windows; Shadowsocks не изменяет DNS-адреса адаптеров.,系统 DNS 信息从活动的 Windows 网络适配器读取;Shadowsocks 不会修改适配器的 DNS 地址。,系統 DNS 資訊會從作用中的 Windows 網路介面卡讀取;Shadowsocks 不會修改介面卡的 DNS 位址。,システム DNS 情報はアクティブな Windows ネットワークアダプターから読み取ります。Shadowsocks はアダプターの DNS アドレスを変更しません。,시스템 DNS 정보는 활성 Windows 네트워크 어댑터에서 읽으며 Shadowsocks는 어댑터의 DNS 주소를 변경하지 않습니다.,Les informations DNS système sont lues depuis les adaptateurs réseau Windows actifs ; Shadowsocks ne modifie pas les adresses DNS des adaptateurs. +System DNS settings,Настройки системного DNS,系统 DNS 设置,系統 DNS 設定,システム DNS 設定,시스템 DNS 설정,Paramètres DNS système +Windows DNS servers from active network adapters are used without modification.,DNS-серверы активных сетевых адаптеров Windows используются без изменений.,直接使用活动 Windows 网络适配器配置的 DNS 服务器,不做修改。,直接使用作用中 Windows 網路介面卡設定的 DNS 伺服器,不做修改。,アクティブな Windows ネットワークアダプターの DNS サーバーを変更せずに使用します。,활성 Windows 네트워크 어댑터의 DNS 서버를 변경하지 않고 사용합니다.,Les serveurs DNS des adaptateurs réseau Windows actifs sont utilisés sans modification. +Direct DNS settings,Настройки прямого DNS,直连 DNS 设置,直接 DNS 設定,直接 DNS 設定,직접 DNS 설정,Paramètres DNS direct +Captured DNS queries are sent directly to their original destination and bypass Shadowsocks.,Перехваченные DNS-запросы отправляются напрямую исходному назначению в обход Shadowsocks.,捕获的 DNS 查询直接发送到原始目标并绕过 Shadowsocks。,擷取的 DNS 查詢會直接傳送到原始目的地並繞過 Shadowsocks。,キャプチャした DNS クエリは元の宛先へ直接送信され、Shadowsocks を経由しません。,캡처된 DNS 질의는 원래 대상에 직접 전송되어 Shadowsocks를 우회합니다.,Les requêtes DNS capturées sont envoyées directement à leur destination d’origine en contournant Shadowsocks. +DNS through Shadowsocks settings,Настройки DNS через Shadowsocks,通过 Shadowsocks 的 DNS 设置,透過 Shadowsocks 的 DNS 設定,Shadowsocks 経由 DNS 設定,Shadowsocks 경유 DNS 설정,Paramètres DNS via Shadowsocks +Captured DNS queries use the active Shadowsocks route; no separate resolver endpoint is required.,Перехваченные DNS-запросы используют активный маршрут Shadowsocks; отдельный адрес резолвера не требуется.,捕获的 DNS 查询使用当前 Shadowsocks 路由;无需单独配置解析器端点。,擷取的 DNS 查詢會使用目前的 Shadowsocks 路由;不需要另外設定解析器端點。,キャプチャした DNS クエリはアクティブな Shadowsocks ルートを使用します。個別のリゾルバーエンドポイントは不要です。,캡처된 DNS 질의는 활성 Shadowsocks 경로를 사용하며 별도의 리졸버 엔드포인트가 필요하지 않습니다.,Les requêtes DNS capturées utilisent la route Shadowsocks active ; aucun point de terminaison de résolveur distinct n’est requis. +Custom DoH settings,Настройки пользовательского DoH,自定义 DoH 设置,自訂 DoH 設定,カスタム DoH 設定,사용자 지정 DoH 설정,Paramètres DoH personnalisés +DNS queries are sent as DNS-over-HTTPS to the configured endpoint.,DNS-запросы отправляются на настроенный адрес по DNS-over-HTTPS.,DNS 查询通过 DNS-over-HTTPS 发送到配置的端点。,DNS 查詢會透過 DNS-over-HTTPS 傳送到設定的端點。,DNS クエリは DNS-over-HTTPS として設定済みエンドポイントへ送信されます。,DNS 질의는 DNS-over-HTTPS로 구성된 엔드포인트에 전송됩니다.,Les requêtes DNS sont envoyées en DNS-over-HTTPS au point de terminaison configuré. +No active network adapters detected.,Активные сетевые адаптеры не обнаружены.,未检测到活动的网络适配器。,未偵測到作用中的網路介面卡。,アクティブなネットワークアダプターが見つかりません。,활성 네트워크 어댑터를 찾지 못했습니다.,Aucun adaptateur réseau actif détecté. +No active DNS servers detected.,Активные DNS-серверы не обнаружены.,未检测到活动的 DNS 服务器。,未偵測到作用中的 DNS 伺服器。,アクティブな DNS サーバーが見つかりません。,활성 DNS 서버를 찾지 못했습니다.,Aucun serveur DNS actif détecté. +Unavailable in User Mode,Недоступно в пользовательском режиме,用户模式下不可用,使用者模式下不可用,ユーザーモードでは利用不可,사용자 모드에서 사용할 수 없음,Indisponible en mode Utilisateur +Paused in Game Mode,Приостановлено в игровом режиме,游戏模式下已暂停,遊戲模式下已暫停,ゲームモードで一時停止,게임 모드에서 일시 중지됨,Suspendue en mode Jeu +Custom DoH settings applied.,Настройки пользовательского DoH применены.,自定义 DoH 设置已应用。,自訂 DoH 設定已套用。,カスタム DoH 設定を適用しました。,사용자 지정 DoH 설정이 적용되었습니다.,Paramètres DoH personnalisés appliqués. +Enter a valid HTTPS DoH URL to enable Custom DoH.,"Введите корректный HTTPS URL DoH, чтобы включить пользовательский DoH.",输入有效的 HTTPS DoH URL 以启用自定义 DoH。,輸入有效的 HTTPS DoH URL 以啟用自訂 DoH。,カスタム DoH を有効にするには有効な HTTPS DoH URL を入力してください。,사용자 지정 DoH를 활성화하려면 올바른 HTTPS DoH URL을 입력하세요.,Saisissez une URL DoH HTTPS valide pour activer le DoH personnalisé. +Use DNS servers configured by Windows without Shadowsocks DNS redirection.,"Использовать DNS-серверы, настроенные в Windows, без перенаправления DNS средствами Shadowsocks.",使用 Windows 配置的 DNS 服务器,不进行 Shadowsocks DNS 重定向。,使用 Windows 設定的 DNS 伺服器,不進行 Shadowsocks DNS 重新導向。,Windows で設定された DNS サーバーを Shadowsocks の DNS リダイレクトなしで使用します。,Windows에 구성된 DNS 서버를 Shadowsocks DNS 리디렉션 없이 사용합니다.,Utiliser les serveurs DNS configurés par Windows sans redirection DNS par Shadowsocks. +Send captured DNS traffic directly to its original destination. Transparent enforcement requires Administrator Mode.,Отправлять перехваченный DNS-трафик напрямую исходному назначению. Для прозрачного применения требуется режим администратора.,将捕获的 DNS 流量直接发送到原始目标。透明强制应用需要管理员模式。,將擷取的 DNS 流量直接傳送到原始目的地。透明強制套用需要系統管理員模式。,キャプチャした DNS トラフィックを元の宛先へ直接送信します。透過的な強制には管理者モードが必要です。,캡처된 DNS 트래픽을 원래 대상에 직접 전송합니다. 투명 적용에는 관리자 모드가 필요합니다.,Envoyer le trafic DNS capturé directement à sa destination d’origine. L’application transparente nécessite le mode Administrateur. +Route captured DNS traffic through the active Shadowsocks server. Transparent enforcement requires Administrator Mode.,Маршрутизировать перехваченный DNS-трафик через активный сервер Shadowsocks. Для прозрачного применения требуется режим администратора.,通过当前 Shadowsocks 服务器路由捕获的 DNS 流量。透明强制应用需要管理员模式。,透過目前的 Shadowsocks 伺服器路由擷取的 DNS 流量。透明強制套用需要系統管理員模式。,キャプチャした DNS トラフィックをアクティブな Shadowsocks サーバー経由でルーティングします。透過的な強制には管理者モードが必要です。,캡처된 DNS 트래픽을 활성 Shadowsocks 서버를 통해 라우팅합니다. 투명 적용에는 관리자 모드가 필요합니다.,Acheminer le trafic DNS capturé via le serveur Shadowsocks actif. L’application transparente nécessite le mode Administrateur. +Convert captured DNS queries to DNS-over-HTTPS using the configured HTTPS endpoint.,Преобразовывать перехваченные DNS-запросы в DNS-over-HTTPS с использованием настроенного HTTPS-адреса.,使用配置的 HTTPS 端点将捕获的 DNS 查询转换为 DNS-over-HTTPS。,使用設定的 HTTPS 端點將擷取的 DNS 查詢轉換為 DNS-over-HTTPS。,設定した HTTPS エンドポイントを使用してキャプチャした DNS クエリを DNS-over-HTTPS に変換します。,구성된 HTTPS 엔드포인트를 사용해 캡처된 DNS 질의를 DNS-over-HTTPS로 변환합니다.,Convertir les requêtes DNS capturées en DNS-over-HTTPS via le point de terminaison HTTPS configuré. +Use DNSCrypt Proxy for Shadowsocks-managed DNS and transparent DNS capture when available.,"Использовать DNSCrypt Proxy для DNS, управляемого Shadowsocks, и для прозрачного перехвата DNS, когда он доступен.",使用 DNSCrypt Proxy 处理 Shadowsocks 管理的 DNS,并在可用时进行透明 DNS 捕获。,使用 DNSCrypt Proxy 處理 Shadowsocks 管理的 DNS,並在可用時進行透明 DNS 擷取。,Shadowsocks 管理の DNS と、利用可能な場合の透過 DNS キャプチャに DNSCrypt Proxy を使用します。,Shadowsocks 관리 DNS 및 사용 가능한 경우 투명 DNS 캡처에 DNSCrypt Proxy를 사용합니다.,Utiliser DNSCrypt Proxy pour le DNS géré par Shadowsocks et la capture DNS transparente lorsqu’elle est disponible. +Enter an absolute HTTPS DNS-over-HTTPS endpoint. Changes are applied automatically when you leave the field.,Введите абсолютный HTTPS-адрес DNS-over-HTTPS. Изменения применяются автоматически при выходе из поля.,输入绝对 HTTPS DNS-over-HTTPS 端点。离开该字段时会自动应用更改。,輸入絕對 HTTPS DNS-over-HTTPS 端點。離開此欄位時會自動套用變更。,絶対 HTTPS の DNS-over-HTTPS エンドポイントを入力します。フィールドからフォーカスを外すと変更が自動適用されます。,절대 HTTPS DNS-over-HTTPS 엔드포인트를 입력하세요. 필드에서 포커스를 벗어나면 변경 사항이 자동 적용됩니다.,Saisissez un point de terminaison DNS-over-HTTPS HTTPS absolu. Les modifications sont appliquées automatiquement lorsque vous quittez le champ. +"Cancel the current DNSCrypt install, update, validation, or runtime operation.","Отменить текущую установку, обновление, проверку или runtime-операцию DNSCrypt.",取消当前 DNSCrypt 安装、更新、验证或运行时操作。,取消目前 DNSCrypt 安裝、更新、驗證或執行階段操作。,現在の DNSCrypt のインストール、更新、検証、またはランタイム操作をキャンセルします。,"현재 DNSCrypt 설치, 업데이트, 검증 또는 런타임 작업을 취소합니다.","Annuler l’installation, la mise à jour, la validation ou l’opération runtime DNSCrypt en cours." +"Download, verify, and install the official Windows x64 DNSCrypt Proxy release.","Скачать, проверить и установить официальный релиз DNSCrypt Proxy для Windows x64.",下载、验证并安装官方 Windows x64 DNSCrypt Proxy 版本。,下載、驗證並安裝官方 Windows x64 DNSCrypt Proxy 版本。,公式 Windows x64 DNSCrypt Proxy リリースをダウンロード、検証、インストールします。,"공식 Windows x64 DNSCrypt Proxy 릴리스를 다운로드, 검증 및 설치합니다.","Télécharger, vérifier et installer la version officielle Windows x64 de DNSCrypt Proxy." +Check the official DNSCrypt Proxy release channel for a newer version.,Проверить официальный канал релизов DNSCrypt Proxy на наличие новой версии.,检查官方 DNSCrypt Proxy 发布渠道是否有新版本。,檢查官方 DNSCrypt Proxy 發布管道是否有新版本。,公式 DNSCrypt Proxy リリースチャネルで新しいバージョンを確認します。,공식 DNSCrypt Proxy 릴리스 채널에서 새 버전을 확인합니다.,Vérifier si une nouvelle version est disponible sur le canal officiel de DNSCrypt Proxy. +Install the newest verified DNSCrypt Proxy version and keep the previous version for rollback.,Установить последнюю проверенную версию DNSCrypt Proxy и сохранить предыдущую для отката.,安装最新的已验证 DNSCrypt Proxy 版本,并保留上一版本用于回滚。,安裝最新的已驗證 DNSCrypt Proxy 版本,並保留上一版本以供回復。,最新の検証済み DNSCrypt Proxy をインストールし、ロールバック用に前のバージョンを保持します。,최신 검증된 DNSCrypt Proxy 버전을 설치하고 롤백을 위해 이전 버전을 유지합니다.,Installer la dernière version vérifiée de DNSCrypt Proxy et conserver la précédente pour un retour arrière. +Restart DNSCrypt Proxy with the currently saved settings.,Перезапустить DNSCrypt Proxy с текущими сохранёнными настройками.,使用当前已保存设置重新启动 DNSCrypt Proxy。,使用目前已儲存的設定重新啟動 DNSCrypt Proxy。,現在保存されている設定で DNSCrypt Proxy を再起動します。,현재 저장된 설정으로 DNSCrypt Proxy를 다시 시작합니다.,Redémarrer DNSCrypt Proxy avec les paramètres actuellement enregistrés. +Send a real DNS query through the local DNSCrypt listener and validate the response.,Отправить реальный DNS-запрос через локальный listener DNSCrypt и проверить ответ.,通过本地 DNSCrypt 监听器发送真实 DNS 查询并验证响应。,透過本機 DNSCrypt 監聽器傳送實際 DNS 查詢並驗證回應。,ローカル DNSCrypt リスナー経由で実際の DNS クエリを送信し、応答を検証します。,로컬 DNSCrypt 리스너를 통해 실제 DNS 질의를 보내고 응답을 검증합니다.,Envoyer une véritable requête DNS via l’écouteur DNSCrypt local et valider la réponse. +"Download and verify the current DNSCrypt Proxy version again, then replace the installed copy.","Повторно скачать и проверить текущую версию DNSCrypt Proxy, затем заменить установленную копию.",重新下载并验证当前 DNSCrypt Proxy 版本,然后替换已安装副本。,重新下載並驗證目前 DNSCrypt Proxy 版本,然後取代已安裝的副本。,現在の DNSCrypt Proxy バージョンを再ダウンロードして検証し、インストール済みコピーを置き換えます。,현재 DNSCrypt Proxy 버전을 다시 다운로드하고 검증한 뒤 설치된 복사본을 교체합니다.,"Télécharger et vérifier à nouveau la version actuelle de DNSCrypt Proxy, puis remplacer la copie installée." +"Stop DNSCrypt Proxy, switch away from DNSCrypt mode, and remove the downloaded component.","Остановить DNSCrypt Proxy, переключиться с режима DNSCrypt и удалить загруженный компонент.",停止 DNSCrypt Proxy,退出 DNSCrypt 模式并删除已下载组件。,停止 DNSCrypt Proxy、離開 DNSCrypt 模式並移除已下載的元件。,DNSCrypt Proxy を停止し、DNSCrypt モードから切り替えて、ダウンロード済みコンポーネントを削除します。,DNSCrypt Proxy를 중지하고 DNSCrypt 모드에서 전환한 뒤 다운로드한 구성 요소를 제거합니다.,"Arrêter DNSCrypt Proxy, quitter le mode DNSCrypt et supprimer le composant téléchargé." +Automatically check for and install verified DNSCrypt Proxy updates in the background.,Автоматически проверять и устанавливать проверенные обновления DNSCrypt Proxy в фоновом режиме.,在后台自动检查并安装已验证的 DNSCrypt Proxy 更新。,在背景自動檢查並安裝已驗證的 DNSCrypt Proxy 更新。,検証済み DNSCrypt Proxy 更新をバックグラウンドで自動確認してインストールします。,검증된 DNSCrypt Proxy 업데이트를 백그라운드에서 자동 확인하고 설치합니다.,Rechercher et installer automatiquement en arrière-plan les mises à jour vérifiées de DNSCrypt Proxy. +Only use resolvers that advertise DNSSEC validation support.,"Использовать только резолверы, заявляющие поддержку проверки DNSSEC.",仅使用声明支持 DNSSEC 验证的解析器。,僅使用宣告支援 DNSSEC 驗證的解析器。,DNSSEC 検証対応を示すリゾルバーのみ使用します。,DNSSEC 검증 지원을 명시한 리졸버만 사용합니다.,Utiliser uniquement les résolveurs annonçant la prise en charge de la validation DNSSEC. +Only use resolvers whose catalog metadata states that they do not keep logs.,"Использовать только резолверы, для которых в каталоге указано отсутствие журналирования.",仅使用目录元数据声明不保留日志的解析器。,僅使用目錄中宣告不保留記錄的解析器。,カタログのメタデータでログを保持しないと示されているリゾルバーのみ使用します。,카탈로그 메타데이터에 로그를 보관하지 않는다고 표시된 리졸버만 사용합니다.,Utiliser uniquement les résolveurs dont les métadonnées indiquent qu’ils ne conservent pas de journaux. +Only use resolvers whose catalog metadata states that they do not filter DNS answers.,"Использовать только резолверы, для которых в каталоге указано отсутствие фильтрации DNS-ответов.",仅使用目录元数据声明不会过滤 DNS 响应的解析器。,僅使用目錄中宣告不會過濾 DNS 回應的解析器。,カタログのメタデータで DNS 応答をフィルタリングしないと示されているリゾルバーのみ使用します。,카탈로그 메타데이터에 DNS 응답을 필터링하지 않는다고 표시된 리졸버만 사용합니다.,Utiliser uniquement les résolveurs dont les métadonnées indiquent qu’ils ne filtrent pas les réponses DNS. +Allow DNSCrypt Proxy to use resolver endpoints reachable over IPv6.,"Разрешить DNSCrypt Proxy использовать адреса резолверов, доступные по IPv6.",允许 DNSCrypt Proxy 使用可通过 IPv6 访问的解析器端点。,允許 DNSCrypt Proxy 使用可透過 IPv6 存取的解析器端點。,DNSCrypt Proxy が IPv6 で到達可能なリゾルバーエンドポイントを使用できるようにします。,DNSCrypt Proxy가 IPv6로 접근 가능한 리졸버 엔드포인트를 사용하도록 허용합니다.,Autoriser DNSCrypt Proxy à utiliser des points de terminaison de résolveur accessibles en IPv6. +Send DNSCrypt Proxy upstream connections through the local Shadowsocks proxy. Server and forward-proxy endpoints must use IP addresses to avoid bootstrap recursion.,"Направлять исходящие соединения DNSCrypt Proxy через локальный прокси Shadowsocks. Сервер Shadowsocks и forward proxy должны задаваться IP-адресами, чтобы избежать рекурсии bootstrap DNS.",通过本地 Shadowsocks 代理发送 DNSCrypt Proxy 上游连接。服务器和转发代理端点必须使用 IP 地址以避免 DNS 引导递归。,透過本機 Shadowsocks Proxy 傳送 DNSCrypt Proxy 上游連線。伺服器與轉送 Proxy 端點必須使用 IP 位址以避免 DNS 啟動遞迴。,DNSCrypt Proxy の上流接続をローカル Shadowsocks プロキシ経由で送信します。DNS ブートストラップ再帰を避けるため、サーバーとフォワードプロキシのエンドポイントには IP アドレスが必要です。,DNSCrypt Proxy 업스트림 연결을 로컬 Shadowsocks 프록시를 통해 전송합니다. DNS 부트스트랩 재귀를 방지하려면 서버 및 전달 프록시 엔드포인트가 IP 주소를 사용해야 합니다.,Acheminer les connexions amont de DNSCrypt Proxy via le proxy Shadowsocks local. Les points de terminaison du serveur et du proxy de transfert doivent utiliser des adresses IP afin d’éviter une récursion d’amorçage DNS. +Block intercepted DNS instead of silently falling back to plaintext DNS when DNSCrypt is unavailable.,Блокировать перехваченный DNS вместо незаметного отката на незашифрованный DNS при недоступности DNSCrypt.,当 DNSCrypt 不可用时阻止捕获的 DNS,而不是静默回退到明文 DNS。,當 DNSCrypt 不可用時封鎖擷取的 DNS,而不是靜默回退到明文 DNS。,DNSCrypt が利用できない場合、平文 DNS へ暗黙にフォールバックせず、キャプチャした DNS をブロックします。,DNSCrypt를 사용할 수 없을 때 평문 DNS로 조용히 폴백하지 않고 캡처된 DNS를 차단합니다.,Bloquer le DNS intercepté au lieu de revenir silencieusement au DNS en clair lorsque DNSCrypt est indisponible. +Choose one or more DNSCrypt resolvers manually from the signed upstream catalog.,Вручную выбрать один или несколько DNSCrypt-резолверов из подписанного upstream-каталога.,从签名的上游目录中手动选择一个或多个 DNSCrypt 解析器。,從已簽署的上游目錄中手動選擇一個或多個 DNSCrypt 解析器。,署名済み上流カタログから 1 つ以上の DNSCrypt リゾルバーを手動選択します。,서명된 업스트림 카탈로그에서 하나 이상의 DNSCrypt 리졸버를 수동으로 선택합니다.,Choisir manuellement un ou plusieurs résolveurs DNSCrypt depuis le catalogue amont signé. +Reload resolver metadata and measured latency from DNSCrypt Proxy's signed upstream catalog.,Повторно загрузить метаданные резолверов и измеренную задержку из подписанного upstream-каталога DNSCrypt Proxy.,从 DNSCrypt Proxy 的签名上游目录重新加载解析器元数据和测得的延迟。,從 DNSCrypt Proxy 的已簽署上游目錄重新載入解析器中繼資料與測得的延遲。,DNSCrypt Proxy の署名済み上流カタログからリゾルバーのメタデータと測定済み遅延を再読み込みします。,DNSCrypt Proxy의 서명된 업스트림 카탈로그에서 리졸버 메타데이터와 측정된 지연 시간을 다시 불러옵니다.,Recharger les métadonnées des résolveurs et la latence mesurée depuis le catalogue amont signé de DNSCrypt Proxy. +Select the resolvers DNSCrypt Proxy may use in manual-selection mode.,"Выберите резолверы, которые DNSCrypt Proxy может использовать в режиме ручного выбора.",选择 DNSCrypt Proxy 在手动选择模式下可使用的解析器。,選擇 DNSCrypt Proxy 在手動選擇模式下可使用的解析器。,手動選択モードで DNSCrypt Proxy が使用できるリゾルバーを選びます。,수동 선택 모드에서 DNSCrypt Proxy가 사용할 수 있는 리졸버를 선택합니다.,Sélectionner les résolveurs que DNSCrypt Proxy peut utiliser en mode de sélection manuelle. +DNS operation failed. See Logs for details.,Операция DNS завершилась ошибкой. Подробности см. в журнале.,DNS 操作失败。详细信息请查看日志。,DNS 操作失敗。詳細資訊請查看記錄。,DNS 操作に失敗しました。詳細はログを確認してください。,DNS 작업에 실패했습니다. 자세한 내용은 로그를 확인하세요.,Échec de l’opération DNS. Consultez les journaux pour plus de détails. +Show DNS Logs,Показывать логи DNS,显示 DNS 日志,顯示 DNS 記錄,DNS ログを表示,DNS 로그 표시,Afficher les journaux DNS +Include DNSCrypt and DoH lifecycle events and upstream warnings. Resolver probe diagnostics and RTT details are shown only when Verbose Logging is also enabled. Application-level DNS errors remain logged even when this option is off.,Включать события жизненного цикла DNSCrypt и DoH и предупреждения upstream. Диагностика перебора резолверов и RTT показывается только при одновременно включённом «Подробном журнале». Ошибки DNS уровня приложения продолжают записываться даже при отключённой опции.,包含 DNSCrypt 和 DoH 生命周期事件及上游警告。仅当同时启用“详细记录日志”时才显示解析器探测诊断和 RTT 详情。即使关闭此选项,应用级 DNS 错误仍会记录。,包含 DNSCrypt 和 DoH 生命週期事件及上游警告。只有同時啟用「詳細資訊記錄」時才顯示解析器探測診斷與 RTT 詳細資料。即使關閉此選項,應用程式層級的 DNS 錯誤仍會記錄。,DNSCrypt/DoH のライフサイクルイベントと上流警告を記録します。リゾルバープローブ診断と RTT の詳細は「詳細なログを記録」も有効な場合にのみ表示されます。この設定をオフにしてもアプリケーションレベルの DNS エラーは記録されます。,DNSCrypt 및 DoH 수명 주기 이벤트와 업스트림 경고를 기록합니다. 리졸버 탐색 진단과 RTT 세부 정보는 '자세한 로깅 사용'도 활성화된 경우에만 표시됩니다. 이 옵션을 꺼도 애플리케이션 수준 DNS 오류는 계속 기록됩니다.,Inclure les événements de cycle de vie DNSCrypt/DoH et les avertissements amont. Les diagnostics de sondage des résolveurs et les détails RTT ne sont affichés que si la journalisation détaillée est également activée. Les erreurs DNS de niveau application restent journalisées même si cette option est désactivée. +DoH provider,Провайдер DoH,DoH 提供商,DoH 提供者,DoH プロバイダー,DoH 공급자,Fournisseur DoH +Choose a well-known RFC 8484 DNS-over-HTTPS provider or select Custom to enter your own HTTPS endpoint.,"Выберите известного провайдера DNS-over-HTTPS по RFC 8484 или выберите «Пользовательский», чтобы указать собственный HTTPS endpoint.",选择常用的 RFC 8484 DNS-over-HTTPS 提供商,或选择“自定义”输入自己的 HTTPS 端点。,選擇常用的 RFC 8484 DNS-over-HTTPS 提供者,或選擇「自訂」輸入自己的 HTTPS 端點。,一般的な RFC 8484 DNS-over-HTTPS プロバイダーを選ぶか、「カスタム」を選択して独自の HTTPS エンドポイントを入力します。,널리 알려진 RFC 8484 DNS-over-HTTPS 공급자를 선택하거나 사용자 지정을 선택해 자체 HTTPS 엔드포인트를 입력합니다.,Choisissez un fournisseur DNS-over-HTTPS RFC 8484 connu ou sélectionnez Personnalisé pour saisir votre propre point de terminaison HTTPS. +Custom,Пользовательский,自定义,自訂,カスタム,사용자 지정,Personnalisé +DNS-over-HTTPS settings,Настройки DNS-over-HTTPS,DNS-over-HTTPS 设置,DNS-over-HTTPS 設定,DNS-over-HTTPS 設定,DNS-over-HTTPS 설정,Paramètres DNS-over-HTTPS +DoH URL,URL DoH,DoH URL,DoH URL,DoH URL,DoH URL,URL DoH +Quad9 Secure,Quad9 с защитой,Quad9 安全版,Quad9 安全版,Quad9 セキュア,Quad9 보안,Quad9 sécurisé +Quad9 Unfiltered,Quad9 без фильтрации,Quad9 无过滤,Quad9 無過濾,Quad9 フィルターなし,Quad9 필터 없음,Quad9 sans filtrage +AdGuard DNS Unfiltered,AdGuard DNS без фильтрации,AdGuard DNS 无过滤,AdGuard DNS 無過濾,AdGuard DNS フィルターなし,AdGuard DNS 필터 없음,AdGuard DNS sans filtrage +DNS server address,Адрес DNS-сервера,DNS 服务器地址,DNS 伺服器位址,DNS サーバーアドレス,DNS 서버 주소,Adresse du serveur DNS +Enter an IPv4 or IPv6 DNS server address. Leave it empty to keep the application's original DNS destination.,"Введите IPv4- или IPv6-адрес DNS-сервера. Оставьте поле пустым, чтобы сохранить исходный DNS-адрес приложения.",输入 IPv4 或 IPv6 DNS 服务器地址。留空则保留应用程序原始的 DNS 目标地址。,輸入 IPv4 或 IPv6 DNS 伺服器位址。留空則保留應用程式原始的 DNS 目的位址。,IPv4 または IPv6 の DNS サーバーアドレスを入力します。空欄にするとアプリケーション本来の DNS 宛先を維持します。,IPv4 또는 IPv6 DNS 서버 주소를 입력하세요. 비워 두면 애플리케이션의 원래 DNS 대상 주소를 유지합니다.,Saisissez l’adresse IPv4 ou IPv6 d’un serveur DNS. Laissez vide pour conserver la destination DNS d’origine de l’application. +"When an address is configured, Administrator Mode transparently redirects UDP/TCP DNS queries to that resolver. Leave it empty to use the original destination.","Если адрес задан, режим администратора прозрачно перенаправляет DNS-запросы UDP/TCP на этот резолвер. Оставьте поле пустым, чтобы использовать исходный адрес назначения.",配置地址后,管理员模式会将 UDP/TCP DNS 查询透明重定向到该解析器。留空则使用原始目标地址。,設定位址後,系統管理員模式會將 UDP/TCP DNS 查詢透明重新導向至該解析器。留空則使用原始目的位址。,アドレスを設定すると、管理者モードは UDP/TCP DNS クエリをそのリゾルバーへ透過的にリダイレクトします。空欄では元の宛先を使用します。,주소를 설정하면 관리자 모드가 UDP/TCP DNS 질의를 해당 리졸버로 투명하게 리디렉션합니다. 비워 두면 원래 대상을 사용합니다.,"Lorsqu’une adresse est configurée, le mode Administrateur redirige de façon transparente les requêtes DNS UDP/TCP vers ce résolveur. Laissez vide pour utiliser la destination d’origine." +Route Custom DoH through Shadowsocks,Направлять пользовательский DoH через Shadowsocks,通过 Shadowsocks 路由自定义 DoH,透過 Shadowsocks 路由自訂 DoH,カスタム DoH を Shadowsocks 経由にする,사용자 지정 DoH를 Shadowsocks로 라우팅,Acheminer le DoH personnalisé via Shadowsocks +Send the HTTPS connection to the Custom DoH provider through the local Shadowsocks SOCKS5 proxy instead of connecting directly.,Отправлять HTTPS-соединение к пользовательскому DoH-провайдеру через локальный SOCKS5-прокси Shadowsocks вместо прямого подключения.,通过本地 Shadowsocks SOCKS5 代理连接自定义 DoH 提供商,而不是直接连接。,透過本機 Shadowsocks SOCKS5 Proxy 連線到自訂 DoH 提供者,而不是直接連線。,カスタム DoH プロバイダーへの HTTPS 接続を直接接続ではなくローカル Shadowsocks SOCKS5 プロキシ経由で送信します。,사용자 지정 DoH 공급자에 대한 HTTPS 연결을 직접 연결 대신 로컬 Shadowsocks SOCKS5 프록시를 통해 전송합니다.,Envoyer la connexion HTTPS vers le fournisseur DoH personnalisé via le proxy SOCKS5 Shadowsocks local plutôt qu’en connexion directe. +Country,Страна,国家/地区,國家/地區,国,국가,Pays +Address family,Семейство адресов,地址族,位址系列,アドレスファミリ,주소 패밀리,Famille d’adresses +Any address family,Любое семейство адресов,任意地址族,任意位址系列,すべてのアドレスファミリ,모든 주소 패밀리,Toutes les familles d’adresses +DNSSEC only,Только DNSSEC,仅 DNSSEC,僅 DNSSEC,DNSSEC のみ,DNSSEC만,DNSSEC uniquement +No-log only,Только без логов,仅无日志,僅無記錄,ログなしのみ,무로그만,Sans journalisation uniquement +Unfiltered only,Только без фильтрации,仅不过滤,僅不過濾,フィルターなしのみ,필터링 없음만,Sans filtrage uniquement +Show resolvers from a selected country. Country is resolved from the resolver endpoint IP.,Показывать резолверы выбранной страны. Страна определяется по IP-адресу конечной точки резолвера.,显示所选国家/地区的解析器。国家/地区根据解析器端点 IP 确定。,顯示所選國家/地區的解析器。國家/地區依解析器端點 IP 判定。,選択した国のリゾルバーを表示します。国はリゾルバーのエンドポイント IP から判定されます。,선택한 국가의 리졸버를 표시합니다. 국가는 리졸버 엔드포인트 IP로 판별됩니다.,Afficher les résolveurs du pays sélectionné. Le pays est déterminé à partir de l’adresse IP du point de terminaison du résolveur. +Filter resolver endpoints by IPv4 or IPv6.,Фильтровать адреса резолверов по IPv4 или IPv6.,按 IPv4 或 IPv6 筛选解析器端点。,依 IPv4 或 IPv6 篩選解析器端點。,リゾルバーのエンドポイントを IPv4 / IPv6 で絞り込みます。,리졸버 엔드포인트를 IPv4 또는 IPv6로 필터링합니다.,Filtrer les points de terminaison des résolveurs par IPv4 ou IPv6. +Only show resolvers that advertise DNSSEC validation.,Показывать только резолверы с заявленной проверкой DNSSEC.,仅显示声明支持 DNSSEC 验证的解析器。,僅顯示宣告支援 DNSSEC 驗證的解析器。,DNSSEC 検証対応を示すリゾルバーのみ表示します。,DNSSEC 검증을 지원한다고 명시한 리졸버만 표시합니다.,Afficher uniquement les résolveurs annonçant la validation DNSSEC. +Only show resolvers whose catalog metadata declares no logging.,"Показывать только резолверы, у которых в каталоге заявлено отсутствие логирования.",仅显示目录元数据声明无日志的解析器。,僅顯示目錄中宣告無記錄的解析器。,カタログでログなしと示されているリゾルバーのみ表示します。,카탈로그에서 무로그로 표시된 리졸버만 표시합니다.,Afficher uniquement les résolveurs dont le catalogue indique l’absence de journalisation. +Only show resolvers that do not filter DNS answers.,"Показывать только резолверы, не фильтрующие DNS-ответы.",仅显示不过滤 DNS 响应的解析器。,僅顯示不過濾 DNS 回應的解析器。,DNS 応答をフィルタリングしないリゾルバーのみ表示します。,DNS 응답을 필터링하지 않는 리졸버만 표시합니다.,Afficher uniquement les résolveurs qui ne filtrent pas les réponses DNS. +Resolver list is loaded from DNSCrypt Proxy's signed upstream catalog. Latency is measured in the background.,Список резолверов загружается из подписанного upstream-каталога DNSCrypt Proxy. Задержка измеряется в фоне.,解析器列表来自 DNSCrypt Proxy 的签名上游目录。延迟在后台测量。,解析器清單來自 DNSCrypt Proxy 的已簽署上游目錄。延遲會在背景量測。,リゾルバー一覧は DNSCrypt Proxy の署名済み上流カタログから読み込みます。遅延はバックグラウンドで測定します。,리졸버 목록은 DNSCrypt Proxy의 서명된 업스트림 카탈로그에서 불러옵니다. 지연 시간은 백그라운드에서 측정합니다.,La liste des résolveurs provient du catalogue amont signé de DNSCrypt Proxy. La latence est mesurée en arrière-plan. +Enter a valid IPv4 or IPv6 DNS server address.,Введите корректный IPv4- или IPv6-адрес DNS-сервера.,请输入有效的 IPv4 或 IPv6 DNS 服务器地址。,請輸入有效的 IPv4 或 IPv6 DNS 伺服器位址。,有効な IPv4 または IPv6 DNS サーバーアドレスを入力してください。,올바른 IPv4 또는 IPv6 DNS 서버 주소를 입력하세요.,Saisissez une adresse IPv4 ou IPv6 de serveur DNS valide. +Custom DNS server applied.,Пользовательский DNS-сервер применён.,自定义 DNS 服务器已应用。,自訂 DNS 伺服器已套用。,カスタム DNS サーバーを適用しました。,사용자 지정 DNS 서버가 적용되었습니다.,Serveur DNS personnalisé appliqué. +Applying DNS settings,Применение настроек DNS,正在应用 DNS 设置,正在套用 DNS 設定,DNS 設定を適用中,DNS 설정 적용 중,Application des paramètres DNS +DNS settings apply timed out; previous runtime was restored.,Применение настроек DNS превысило время ожидания; предыдущий runtime восстановлен.,应用 DNS 设置超时;已恢复之前的运行状态。,套用 DNS 設定逾時;已還原先前的執行狀態。,DNS 設定の適用がタイムアウトしたため、以前のランタイムを復元しました。,DNS 설정 적용 시간이 초과되어 이전 런타임을 복원했습니다.,Délai dépassé lors de l’application des paramètres DNS ; l’exécution précédente a été restaurée. +The selected resolver did not become reachable in time. Previous DNSCrypt settings remain active.,Выбранный резолвер не стал доступен вовремя. Предыдущие настройки DNSCrypt остаются активными.,所选解析器未能及时变为可用。之前的 DNSCrypt 设置仍保持活动。,所選解析器未能及時變為可用。先前的 DNSCrypt 設定仍保持啟用。,選択したリゾルバーが時間内に到達可能になりませんでした。以前の DNSCrypt 設定を維持します。,선택한 리졸버가 제한 시간 내에 연결 가능해지지 않았습니다. 이전 DNSCrypt 설정이 계속 사용됩니다.,Le résolveur sélectionné n’est pas devenu joignable à temps. Les paramètres DNSCrypt précédents restent actifs. +Resolver settings were not applied: {0},Настройки резолвера не применены: {0},解析器设置未应用:{0},解析器設定未套用:{0},リゾルバー設定を適用できませんでした: {0},리졸버 설정이 적용되지 않았습니다: {0},Les paramètres du résolveur n’ont pas été appliqués : {0} +DNSCrypt operation failed: {0},Операция DNSCrypt завершилась ошибкой: {0},DNSCrypt 操作失败:{0},DNSCrypt 操作失敗:{0},DNSCrypt 操作に失敗しました: {0},DNSCrypt 작업에 실패했습니다: {0},Échec de l’opération DNSCrypt : {0} +DNS operation failed: {0},Операция DNS завершилась ошибкой: {0},DNS 操作失败:{0},DNS 操作失敗:{0},DNS 操作に失敗しました: {0},DNS 작업에 실패했습니다: {0},Échec de l’opération DNS : {0} +Loaded {0} upstream resolvers. Measuring latency in the background.,Загружено upstream-резолверов: {0}. Задержка измеряется в фоне.,已加载 {0} 个上游解析器。正在后台测量延迟。,已載入 {0} 個上游解析器。正在背景量測延遲。,上流リゾルバーを {0} 件読み込みました。遅延をバックグラウンドで測定しています。,업스트림 리졸버 {0}개를 불러왔습니다. 백그라운드에서 지연 시간을 측정합니다.,{0} résolveurs amont chargés. Mesure de la latence en arrière-plan. +Latency available for {0} of {1} resolver(s).,Задержка измерена для {0} из {1} резолверов.,已有 {0}/{1} 个解析器的延迟数据。,已有 {0}/{1} 個解析器的延遲資料。,{1} 件中 {0} 件のリゾルバーで遅延を取得しました。,리졸버 {1}개 중 {0}개의 지연 시간을 확인했습니다.,Latence disponible pour {0} résolveur(s) sur {1}. +Resolver latency measurement failed: {0},Не удалось измерить задержку резолверов: {0},解析器延迟测量失败:{0},解析器延遲量測失敗:{0},リゾルバーの遅延測定に失敗しました: {0},리졸버 지연 시간 측정에 실패했습니다: {0},Échec de la mesure de latence des résolveurs : {0} +All countries,Все страны,所有国家/地区,所有國家/地區,すべての国,모든 국가,Tous les pays +Anycast,Anycast,Anycast,Anycast,Anycast,Anycast,Anycast +Resolver list is loaded from DNSCrypt Proxy's signed upstream catalog. Latency is measured in the background; the active resolver uses DNSCrypt Proxy's actual RTT when available.,Список резолверов загружается из подписанного upstream-каталога DNSCrypt Proxy. Задержка измеряется в фоне; для активного резолвера при наличии используется фактический RTT от DNSCrypt Proxy.,解析器列表来自 DNSCrypt Proxy 的签名上游目录。延迟在后台测量;对于活动解析器,如可用则使用 DNSCrypt Proxy 的实际 RTT。,解析器清單來自 DNSCrypt Proxy 的已簽署上游目錄。延遲在背景測量;對於作用中的解析器,如可用則使用 DNSCrypt Proxy 的實際 RTT。,リゾルバー一覧は DNSCrypt Proxy の署名済み上流カタログから読み込まれます。遅延はバックグラウンドで測定し、アクティブなリゾルバーでは利用可能な場合 DNSCrypt Proxy の実 RTT を使用します。,리졸버 목록은 DNSCrypt Proxy의 서명된 업스트림 카탈로그에서 불러옵니다. 지연 시간은 백그라운드에서 측정하며 활성 리졸버는 가능한 경우 DNSCrypt Proxy의 실제 RTT를 사용합니다.,"La liste des résolveurs provient du catalogue amont signé de DNSCrypt Proxy. La latence est mesurée en arrière-plan ; pour le résolveur actif, le RTT réel de DNSCrypt Proxy est utilisé lorsqu’il est disponible." +Select the resolvers DNSCrypt Proxy may use in manual-selection mode. Ping is measured asynchronously; the active resolver is replaced with DNSCrypt Proxy's actual RTT when available.,"Выберите резолверы, которые DNSCrypt Proxy может использовать в ручном режиме. Пинг измеряется асинхронно; для активного резолвера при наличии подставляется фактический RTT DNSCrypt Proxy.",选择 DNSCrypt Proxy 在手动模式下可使用的解析器。Ping 会异步测量;对于活动解析器,如可用则替换为 DNSCrypt Proxy 的实际 RTT。,選擇 DNSCrypt Proxy 在手動模式下可使用的解析器。Ping 會非同步測量;對於作用中的解析器,如可用則替換為 DNSCrypt Proxy 的實際 RTT。,手動モードで DNSCrypt Proxy が使用できるリゾルバーを選択します。Ping は非同期に測定され、アクティブなリゾルバーでは利用可能な場合 DNSCrypt Proxy の実 RTT に置き換えられます。,수동 모드에서 DNSCrypt Proxy가 사용할 리졸버를 선택합니다. Ping은 비동기로 측정되며 활성 리졸버는 가능한 경우 DNSCrypt Proxy의 실제 RTT로 대체됩니다.,"Sélectionnez les résolveurs utilisables par DNSCrypt Proxy en mode manuel. Le ping est mesuré de façon asynchrone ; pour le résolveur actif, il est remplacé par le RTT réel de DNSCrypt Proxy lorsqu’il est disponible." +Send captured DNS traffic directly. You can keep the original destination or specify an IPv4/IPv6 DNS server; transparent enforcement requires Administrator Mode.,Отправлять перехваченный DNS напрямую. Можно сохранить исходный адрес назначения или задать IPv4/IPv6 DNS-сервер; прозрачное применение требует режима администратора.,直接发送捕获的 DNS 流量。可以保留原始目标,也可以指定 IPv4/IPv6 DNS 服务器;透明强制需要管理员模式。,直接傳送擷取的 DNS 流量。可以保留原始目的地,也可以指定 IPv4/IPv6 DNS 伺服器;透明套用需要系統管理員模式。,キャプチャした DNS トラフィックを直接送信します。元の宛先を維持するか IPv4/IPv6 DNS サーバーを指定できます。透過的な適用には管理者モードが必要です。,캡처된 DNS 트래픽을 직접 보냅니다. 원래 대상을 유지하거나 IPv4/IPv6 DNS 서버를 지정할 수 있으며 투명 적용에는 관리자 모드가 필요합니다.,Envoyer directement le trafic DNS intercepté. Vous pouvez conserver la destination d’origine ou spécifier un serveur DNS IPv4/IPv6 ; l’application transparente nécessite le mode Administrateur. +Reload resolver metadata from DNSCrypt Proxy's signed upstream catalog. Latency is measured separately in the background.,Повторно загрузить метаданные резолверов из подписанного upstream-каталога DNSCrypt Proxy. Задержка измеряется отдельно в фоне.,从 DNSCrypt Proxy 的签名上游目录重新加载解析器元数据。延迟会在后台单独测量。,從 DNSCrypt Proxy 的已簽署上游目錄重新載入解析器中繼資料。延遲會在背景另外測量。,DNSCrypt Proxy の署名済み上流カタログからリゾルバーのメタデータを再読み込みします。遅延は別途バックグラウンドで測定されます。,DNSCrypt Proxy의 서명된 업스트림 카탈로그에서 리졸버 메타데이터를 다시 불러옵니다. 지연 시간은 백그라운드에서 별도로 측정됩니다.,Recharger les métadonnées des résolveurs depuis le catalogue amont signé de DNSCrypt Proxy. La latence est mesurée séparément en arrière-plan. +Original DNS destination,Исходный адрес DNS,原始 DNS 目标,原始 DNS 目的地,元の DNS 宛先,원래 DNS 대상,Destination DNS d’origine +DNS provider,DNS-провайдер,DNS 提供商,DNS 提供者,DNS プロバイダー,DNS 제공자,Fournisseur DNS +"Choose a known DNS provider, keep the original DNS destination, or select Custom to enter primary and fallback addresses.","Выберите известного DNS-провайдера, сохраните исходный DNS-адрес назначения или выберите «Пользовательский», чтобы задать основной и резервный адреса.",选择已知 DNS 提供商、保留原始 DNS 目标,或选择“自定义”以输入主 DNS 和备用 DNS 地址。,選擇已知 DNS 提供者、保留原始 DNS 目的地,或選擇「自訂」以輸入主要與備用 DNS 位址。,既知の DNS プロバイダーを選択するか、元の DNS 宛先を維持するか、「カスタム」を選択してプライマリとフォールバックのアドレスを入力します。,"알려진 DNS 제공자를 선택하거나 원래 DNS 대상을 유지하거나, 사용자 지정을 선택해 기본 및 보조 주소를 입력하세요.","Choisissez un fournisseur DNS connu, conservez la destination DNS d’origine ou sélectionnez Personnalisé pour saisir les adresses principale et de secours." +Primary DNS server,Основной DNS-сервер,主 DNS 服务器,主要 DNS 伺服器,プライマリ DNS サーバー,기본 DNS 서버,Serveur DNS principal +Fallback DNS server,Резервный DNS-сервер,备用 DNS 服务器,備用 DNS 伺服器,フォールバック DNS サーバー,보조 DNS 서버,Serveur DNS de secours +Primary IPv4 or IPv6 DNS server. In Custom mode this address is used first.,Основной IPv4- или IPv6-адрес DNS-сервера. В пользовательском режиме этот адрес используется первым.,主 IPv4 或 IPv6 DNS 服务器。在自定义模式下优先使用此地址。,主要 IPv4 或 IPv6 DNS 伺服器。在自訂模式下會優先使用此位址。,プライマリ IPv4 または IPv6 DNS サーバー。カスタムモードではこのアドレスが最初に使用されます。,기본 IPv4 또는 IPv6 DNS 서버입니다. 사용자 지정 모드에서는 이 주소를 먼저 사용합니다.,"Serveur DNS IPv4 ou IPv6 principal. En mode Personnalisé, cette adresse est utilisée en premier." +Fallback IPv4 or IPv6 DNS server. UDP queries are retried here when the primary resolver does not answer; TCP falls back when the primary connection fails.,"Резервный IPv4- или IPv6-адрес DNS-сервера. UDP-запросы повторяются через него, если основной резолвер не отвечает; TCP переключается на него при ошибке подключения к основному.",备用 IPv4 或 IPv6 DNS 服务器。主解析器未响应时 UDP 查询会在此重试;主连接失败时 TCP 会切换到此服务器。,備用 IPv4 或 IPv6 DNS 伺服器。主要解析器未回應時 UDP 查詢會在此重試;主要連線失敗時 TCP 會切換至此伺服器。,フォールバック IPv4 または IPv6 DNS サーバー。プライマリが応答しない場合は UDP クエリをここで再試行し、TCP はプライマリ接続失敗時に切り替えます。,"보조 IPv4 또는 IPv6 DNS 서버입니다. 기본 리졸버가 응답하지 않으면 UDP 쿼리를 여기서 재시도하고, TCP는 기본 연결 실패 시 전환합니다.",Serveur DNS IPv4 ou IPv6 de secours. Les requêtes UDP y sont réessayées si le résolveur principal ne répond pas ; TCP bascule en cas d’échec de la connexion principale. +Administrator Mode redirects UDP/TCP DNS queries to the selected resolver. The fallback server is used only after the primary path fails or does not answer in time.,Режим администратора перенаправляет DNS-запросы UDP/TCP на выбранный резолвер. Резервный сервер используется только при ошибке основного пути или отсутствии ответа вовремя.,管理员模式将 UDP/TCP DNS 查询重定向到所选解析器。仅当主路径失败或未及时响应时才使用备用服务器。,系統管理員模式會將 UDP/TCP DNS 查詢重新導向至所選解析器。只有主要路徑失敗或未及時回應時才使用備用伺服器。,管理者モードは UDP/TCP DNS クエリを選択したリゾルバーへリダイレクトします。フォールバックはプライマリ経路が失敗または時間内に応答しない場合のみ使用されます。,관리자 모드는 UDP/TCP DNS 쿼리를 선택한 리졸버로 리디렉션합니다. 보조 서버는 기본 경로가 실패하거나 제시간에 응답하지 않을 때만 사용됩니다.,Le mode Administrateur redirige les requêtes DNS UDP/TCP vers le résolveur choisi. Le serveur de secours n’est utilisé qu’en cas d’échec ou de non-réponse du chemin principal. +Enter a valid primary IPv4 or IPv6 DNS server address.,Введите корректный основной IPv4- или IPv6-адрес DNS-сервера.,请输入有效的主 IPv4 或 IPv6 DNS 服务器地址。,請輸入有效的主要 IPv4 或 IPv6 DNS 伺服器位址。,有効なプライマリ IPv4 または IPv6 DNS サーバーアドレスを入力してください。,올바른 기본 IPv4 또는 IPv6 DNS 서버 주소를 입력하세요.,Saisissez une adresse IPv4 ou IPv6 valide pour le serveur DNS principal. +Enter a valid fallback IPv4 or IPv6 DNS server address.,Введите корректный резервный IPv4- или IPv6-адрес DNS-сервера.,请输入有效的备用 IPv4 或 IPv6 DNS 服务器地址。,請輸入有效的備用 IPv4 或 IPv6 DNS 伺服器位址。,有効なフォールバック IPv4 または IPv6 DNS サーバーアドレスを入力してください。,올바른 보조 IPv4 또는 IPv6 DNS 서버 주소를 입력하세요.,Saisissez une adresse IPv4 ou IPv6 valide pour le serveur DNS de secours. +Enter a primary DNS server before configuring a fallback server.,Перед резервным DNS-сервером укажите основной.,配置备用 DNS 服务器前请先指定主 DNS 服务器。,設定備用 DNS 伺服器前請先指定主要 DNS 伺服器。,フォールバック DNS サーバーを設定する前にプライマリ DNS サーバーを指定してください。,보조 DNS 서버를 구성하기 전에 기본 DNS 서버를 입력하세요.,Indiquez un serveur DNS principal avant de configurer un serveur de secours. +DNS server settings applied.,Настройки DNS-серверов применены.,DNS 服务器设置已应用。,DNS 伺服器設定已套用。,DNS サーバー設定を適用しました。,DNS 서버 설정이 적용되었습니다.,Paramètres des serveurs DNS appliqués. +Route Direct DNS through Shadowsocks,Направлять Direct DNS через Shadowsocks,通过 Shadowsocks 路由 Direct DNS,透過 Shadowsocks 路由 Direct DNS,Direct DNS を Shadowsocks 経由にする,Direct DNS를 Shadowsocks로 라우팅,Acheminer le DNS direct via Shadowsocks +Send queries to the selected Direct DNS primary/fallback servers through the active local Shadowsocks SOCKS5 proxy instead of connecting to them directly.,Отправлять запросы к выбранным основному и резервному Direct DNS-серверам через локальный SOCKS5-прокси Shadowsocks вместо прямого подключения.,通过本地 Shadowsocks SOCKS5 代理向所选 Direct DNS 主/备用服务器发送查询,而不是直接连接。,透過本機 Shadowsocks SOCKS5 Proxy 向所選 Direct DNS 主要/備用伺服器傳送查詢,而不是直接連線。,選択した Direct DNS のプライマリ/フォールバックサーバーへの問い合わせを、直接接続せずローカル Shadowsocks SOCKS5 プロキシ経由で送信します。,선택한 Direct DNS 기본/보조 서버로의 질의를 직접 연결하지 않고 로컬 Shadowsocks SOCKS5 프록시를 통해 전송합니다.,Envoyer les requêtes vers les serveurs DNS directs principal/de secours sélectionnés via le proxy SOCKS5 local de Shadowsocks au lieu de s’y connecter directement. +Direct DNS queries are routed to the selected resolver through Shadowsocks.,Запросы Direct DNS направляются к выбранному резолверу через Shadowsocks.,Direct DNS 查询通过 Shadowsocks 路由到所选解析器。,Direct DNS 查詢會透過 Shadowsocks 路由到所選解析器。,Direct DNS クエリは Shadowsocks 経由で選択したリゾルバーへ送信されます。,Direct DNS 질의가 Shadowsocks를 통해 선택한 리졸버로 라우팅됩니다.,Les requêtes DNS directes sont acheminées vers le résolveur sélectionné via Shadowsocks. +"Filter resolvers by name, country code, country name, or description.","Фильтровать резолверы по имени, коду страны, названию страны или описанию.",按名称、国家/地区代码、国家/地区名称或描述筛选解析器。,依名稱、國家/地區代碼、國家/地區名稱或描述篩選解析器。,名前、国コード、国名、説明でリゾルバーを絞り込みます。,"이름, 국가 코드, 국가 이름 또는 설명으로 리졸버를 필터링합니다.","Filtrer les résolveurs par nom, code pays, nom du pays ou description." +"In Administrator Mode, DNSCrypt always blocks intercepted plaintext DNS while starting, restarting, or unavailable.","В режиме администратора DNSCrypt всегда блокирует перехваченный незашифрованный DNS во время запуска, перезапуска или недоступности.",在管理员模式下,DNSCrypt 在启动、重启或不可用时始终阻止被拦截的明文 DNS。,在系統管理員模式下,DNSCrypt 在啟動、重新啟動或不可用時一律封鎖被攔截的明文 DNS。,管理者モードでは、DNSCrypt は起動中、再起動中、または利用できない場合も、インターセプトした平文 DNS を常にブロックします。,"관리자 모드에서 DNSCrypt는 시작, 재시작 또는 사용 불가 상태 동안 가로챈 평문 DNS를 항상 차단합니다.","En mode administrateur, DNSCrypt bloque toujours le DNS en clair intercepté pendant le démarrage, le redémarrage ou en cas d’indisponibilité." +Protocol,Протокол,协议,協定,プロトコル,프로토콜,Protocole +All protocols,Все протоколы,所有协议,所有協定,すべてのプロトコル,모든 프로토콜,Tous les protocoles +"Automatically select a resolver from DNSCrypt Proxy's signed catalog using the DNSSEC, no-log, unfiltered and address-family settings.","Автоматически выбирать резолвер из подписанного каталога DNSCrypt Proxy с учётом настроек DNSSEC, отсутствия журналирования, отсутствия фильтрации и семейства адресов.",根据 DNSSEC、无日志、无过滤和地址族设置,从 DNSCrypt Proxy 的签名目录中自动选择解析器。,依據 DNSSEC、無日誌、無過濾及位址家族設定,從 DNSCrypt Proxy 的簽署目錄中自動選擇解析器。,DNSSEC、ログなし、フィルタなし、アドレスファミリーの設定に従い、DNSCrypt Proxy の署名済みカタログからリゾルバーを自動選択します。,"DNSSEC, 무로그, 무필터 및 주소 패밀리 설정에 따라 DNSCrypt Proxy의 서명된 카탈로그에서 리졸버를 자동 선택합니다.","Sélectionner automatiquement un résolveur dans le catalogue signé de DNSCrypt Proxy selon les paramètres DNSSEC, sans journalisation, sans filtrage et de famille d’adresses." +Choose one or more DNSCrypt or DoH resolvers manually from the signed upstream catalog.,Вручную выбрать один или несколько DNSCrypt- или DoH-резолверов из подписанного каталога.,从已签名的上游目录中手动选择一个或多个 DNSCrypt 或 DoH 解析器。,從已簽署的上游目錄中手動選擇一個或多個 DNSCrypt 或 DoH 解析器。,署名済みアップストリームカタログから DNSCrypt または DoH リゾルバーを1つ以上手動で選択します。,서명된 업스트림 카탈로그에서 하나 이상의 DNSCrypt 또는 DoH 리졸버를 수동으로 선택합니다.,Choisir manuellement un ou plusieurs résolveurs DNSCrypt ou DoH dans le catalogue amont signé. +Automatic mode selects a filtered resolver from DNSCrypt Proxy's signed catalog.,Автоматический режим выбирает резолвер из подписанного каталога DNSCrypt Proxy с применением фильтров.,自动模式从 DNSCrypt Proxy 的签名目录中选择经过筛选的解析器。,自動模式會從 DNSCrypt Proxy 的簽署目錄中選擇經篩選的解析器。,自動モードでは DNSCrypt Proxy の署名済みカタログからフィルタ条件に合うリゾルバーを選択します。,자동 모드는 DNSCrypt Proxy의 서명된 카탈로그에서 필터 조건에 맞는 리졸버를 선택합니다.,Le mode automatique sélectionne un résolveur filtré dans le catalogue signé de DNSCrypt Proxy. +Automatic filtered selection is active ({0} resolver(s)).,Автоматический выбор с фильтрами активен ({0} резолвер(а/ов)).,自动筛选选择已启用({0} 个解析器)。,自動篩選選擇已啟用({0} 個解析器)。,フィルタ付き自動選択が有効です({0} リゾルバー)。,필터가 적용된 자동 선택이 활성화되었습니다({0}개 리졸버).,La sélection automatique filtrée est active ({0} résolveur(s)). +Automatic filtered resolver is starting.,Автоматически выбранный с учётом фильтров резолвер запускается.,自动筛选的解析器正在启动。,自動篩選的解析器正在啟動。,フィルタ条件で自動選択されたリゾルバーを起動しています。,필터 조건으로 자동 선택된 리졸버를 시작하는 중입니다.,Le résolveur sélectionné automatiquement avec les filtres démarre. +Filter the signed resolver catalog by transport protocol: DNSCrypt or DNS-over-HTTPS.,Фильтровать подписанный каталог резолверов по транспортному протоколу: DNSCrypt или DNS-over-HTTPS.,按传输协议筛选已签名的解析器目录:DNSCrypt 或 DNS-over-HTTPS。,依傳輸協定篩選已簽署的解析器目錄:DNSCrypt 或 DNS-over-HTTPS。,署名済みリゾルバーカタログをトランスポートプロトコル(DNSCrypt または DNS-over-HTTPS)で絞り込みます。,서명된 리졸버 카탈로그를 전송 프로토콜(DNSCrypt 또는 DNS-over-HTTPS)로 필터링합니다.,Filtrer le catalogue signé des résolveurs par protocole de transport : DNSCrypt ou DNS-over-HTTPS. +Select the DNSCrypt or DoH resolvers DNSCrypt Proxy may use in manual-selection mode. Ping is measured asynchronously; the active resolver is replaced with DNSCrypt Proxy's actual RTT when available.,"Выберите DNSCrypt- или DoH-резолверы, которые DNSCrypt Proxy может использовать в ручном режиме. Ping измеряется асинхронно; для активного резолвера используется фактический RTT DNSCrypt Proxy, когда он доступен.",选择 DNSCrypt Proxy 在手动模式下可使用的 DNSCrypt 或 DoH 解析器。Ping 会异步测量;如果可用,活动解析器将显示 DNSCrypt Proxy 的实际 RTT。,選擇 DNSCrypt Proxy 在手動模式下可使用的 DNSCrypt 或 DoH 解析器。Ping 會非同步測量;若可用,作用中解析器將顯示 DNSCrypt Proxy 的實際 RTT。,手動選択モードで DNSCrypt Proxy が使用できる DNSCrypt または DoH リゾルバーを選択します。Ping は非同期で測定され、利用可能な場合はアクティブなリゾルバーに DNSCrypt Proxy の実測 RTT が表示されます。,수동 선택 모드에서 DNSCrypt Proxy가 사용할 DNSCrypt 또는 DoH 리졸버를 선택합니다. Ping은 비동기로 측정되며 가능한 경우 활성 리졸버에는 DNSCrypt Proxy의 실제 RTT가 표시됩니다.,Sélectionnez les résolveurs DNSCrypt ou DoH que DNSCrypt Proxy peut utiliser en mode manuel. Le ping est mesuré de façon asynchrone ; le RTT réel de DNSCrypt Proxy est utilisé pour le résolveur actif lorsqu’il est disponible. +"Filter resolvers by name, protocol, country code, country name, or description.","Фильтровать резолверы по имени, протоколу, коду страны, названию страны или описанию.",按名称、协议、国家/地区代码、国家/地区名称或描述筛选解析器。,依名稱、協定、國家/地區代碼、國家/地區名稱或描述篩選解析器。,名前、プロトコル、国コード、国名、または説明でリゾルバーを絞り込みます。,"이름, 프로토콜, 국가 코드, 국가 이름 또는 설명으로 리졸버를 필터링합니다.","Filtrer les résolveurs par nom, protocole, code pays, nom du pays ou description." +DNS privacy,Конфиденциальность DNS,DNS 隐私,DNS 隱私,DNS プライバシー,DNS 개인정보 보호,Confidentialité DNS +DNS interception,Перехват DNS,DNS 拦截,DNS 攔截,DNS インターセプト,DNS 가로채기,Interception DNS +Upstream,Вышестоящий сервер,上游,上游,アップストリーム,업스트림,Serveur amont +Secure transport,Защищённый транспорт,安全传输,安全傳輸,セキュア転送,보안 전송,Transport sécurisé +Plaintext DNS fallback,Резервный незашифрованный DNS,明文 DNS 回退,明文 DNS 回退,平文 DNS フォールバック,평문 DNS 폴백,Repli DNS en clair +Plaintext bootstrap,Незашифрованный bootstrap DNS,明文引导 DNS,明文啟動 DNS,平文ブートストラップ DNS,평문 부트스트랩 DNS,Bootstrap DNS en clair +Shadowsocks route,Маршрут Shadowsocks,Shadowsocks 路由,Shadowsocks 路由,Shadowsocks ルート,Shadowsocks 경로,Routage Shadowsocks +Privacy self-test,Самопроверка конфиденциальности,隐私自检,隱私自檢,プライバシー自己診断,개인정보 보호 자체 검사,Auto-test de confidentialité +Run privacy self-test,Запустить самопроверку конфиденциальности,运行隐私自检,執行隱私自檢,プライバシー自己診断を実行,개인정보 보호 자체 검사 실행,Exécuter l’auto-test de confidentialité +Administrator Mode required,Требуется режим администратора,需要管理员模式,需要系統管理員模式,管理者モードが必要です,관리자 모드 필요,Mode Administrateur requis +Blocked,Заблокирован,已阻止,已封鎖,ブロック済み,차단됨,Bloqué +Allowed,Разрешён,允许,允許,許可,허용됨,Autorisé +Disabled in DNS runtime,Отключён в рабочем DNS,在 DNS 运行时中已禁用,在 DNS 執行階段中已停用,DNS ランタイムでは無効,DNS 런타임에서 비활성화됨,Désactivé dans le runtime DNS +Through Shadowsocks,Через Shadowsocks,通过 Shadowsocks,透過 Shadowsocks,Shadowsocks 経由,Shadowsocks 경유,Via Shadowsocks +Direct from DNSCrypt Proxy,Напрямую из DNSCrypt Proxy,从 DNSCrypt Proxy 直接连接,從 DNSCrypt Proxy 直接連線,DNSCrypt Proxy から直接,DNSCrypt Proxy에서 직접,Direct depuis DNSCrypt Proxy +Not run,Не запускалась,未运行,未執行,未実行,실행 안 됨,Non exécuté +Passed,Пройдена,通过,通過,成功,통과,Réussi +Testing DNS privacy,Проверка конфиденциальности DNS,正在测试 DNS 隐私,正在測試 DNS 隱私,DNS プライバシーをテスト中,DNS 개인정보 보호 테스트 중,Test de la confidentialité DNS +DNS privacy self-test passed.,Самопроверка конфиденциальности DNS пройдена.,DNS 隐私自检通过。,DNS 隱私自檢通過。,DNS プライバシー自己診断に成功しました。,DNS 개인정보 보호 자체 검사를 통과했습니다.,Auto-test de confidentialité DNS réussi. +Protected DNS path verified: {0} over {1}.,Защищённый DNS-маршрут подтверждён: {0} через {1}.,已验证受保护的 DNS 路径:{0},通过 {1}。,已驗證受保護的 DNS 路徑:{0},透過 {1}。,保護された DNS 経路を確認しました: {0} / {1}。,"보호된 DNS 경로 확인됨: {0}, {1} 사용.",Chemin DNS protégé vérifié : {0} via {1}. +DNS privacy self-test failed: {0},Самопроверка конфиденциальности DNS не пройдена: {0},DNS 隐私自检失败:{0},DNS 隱私自檢失敗:{0},DNS プライバシー自己診断に失敗しました: {0},DNS 개인정보 보호 자체 검사 실패: {0},Échec de l’auto-test de confidentialité DNS : {0} +Unknown DNS privacy failure.,Неизвестная ошибка конфиденциальности DNS.,未知的 DNS 隐私故障。,未知的 DNS 隱私故障。,不明な DNS プライバシー障害です。,알 수 없는 DNS 개인정보 보호 오류입니다.,Erreur de confidentialité DNS inconnue. +DNSCrypt mode is not selected.,Режим DNSCrypt не выбран.,未选择 DNSCrypt 模式。,未選擇 DNSCrypt 模式。,DNSCrypt モードが選択されていません。,DNSCrypt 모드가 선택되지 않았습니다.,Le mode DNSCrypt n’est pas sélectionné. +DNSCrypt runtime health-check failed.,Проверка работоспособности DNSCrypt runtime не пройдена.,DNSCrypt 运行时健康检查失败。,DNSCrypt 執行階段健康檢查失敗。,DNSCrypt ランタイムのヘルスチェックに失敗しました。,DNSCrypt 런타임 상태 검사가 실패했습니다.,Échec du contrôle d’état du runtime DNSCrypt. +Administrator DNS interception is not active.,Перехват DNS в режиме администратора не активен.,管理员 DNS 拦截未激活。,系統管理員 DNS 攔截未啟用。,管理者 DNS インターセプトが有効ではありません。,관리자 DNS 가로채기가 활성화되지 않았습니다.,L’interception DNS en mode Administrateur n’est pas active. +Plaintext DNS fallback is not blocked.,Резервный незашифрованный DNS не заблокирован.,明文 DNS 回退未被阻止。,明文 DNS 回退未被封鎖。,平文 DNS フォールバックがブロックされていません。,평문 DNS 폴백이 차단되지 않았습니다.,Le repli DNS en clair n’est pas bloqué. +The active DNS runtime still permits plaintext bootstrap DNS.,Активный DNS runtime всё ещё допускает незашифрованный bootstrap DNS.,活动 DNS 运行时仍允许明文引导 DNS。,作用中的 DNS 執行階段仍允許明文啟動 DNS。,アクティブな DNS ランタイムが平文ブートストラップ DNS を許可しています。,활성 DNS 런타임이 여전히 평문 부트스트랩 DNS를 허용합니다.,Le runtime DNS actif autorise encore le bootstrap DNS en clair. +The active DNS runtime does not ignore system DNS.,Активный DNS runtime не блокирует использование системного DNS.,活动 DNS 运行时未忽略系统 DNS。,作用中的 DNS 執行階段未忽略系統 DNS。,アクティブな DNS ランタイムがシステム DNS を無視していません。,활성 DNS 런타임이 시스템 DNS를 무시하지 않습니다.,Le runtime DNS actif n’ignore pas le DNS système. +Automatic mode is not pinned to a filtered resolver set from the signed catalog.,"Автоматический режим не закреплён за набором резолверов, отфильтрованным из подписанного каталога.",自动模式未固定到从签名目录筛选出的解析器集合。,自動模式未固定到從簽署目錄篩選出的解析器集合。,自動モードが署名済みカタログからフィルターされたリゾルバーセットに固定されていません。,자동 모드가 서명된 카탈로그에서 필터링된 리졸버 집합에 고정되어 있지 않습니다.,Le mode automatique n’est pas limité à un ensemble de résolveurs filtré depuis le catalogue signé. +"Verify the local DNSCrypt health check, Administrator DNS interception, fail-closed policy, system-DNS isolation and active runtime bootstrap configuration.","Проверить локальный DNSCrypt, перехват DNS в режиме администратора, fail-closed, изоляцию системного DNS и bootstrap-конфигурацию активного runtime.",验证本地 DNSCrypt 健康状态、管理员 DNS 拦截、故障关闭策略、系统 DNS 隔离和活动运行时引导配置。,驗證本機 DNSCrypt 健康狀態、系統管理員 DNS 攔截、故障關閉策略、系統 DNS 隔離與作用中執行階段啟動設定。,ローカル DNSCrypt のヘルスチェック、管理者 DNS インターセプト、fail-closed、システム DNS 分離、アクティブランタイムのブートストラップ設定を確認します。,"로컬 DNSCrypt 상태, 관리자 DNS 가로채기, fail-closed 정책, 시스템 DNS 격리 및 활성 런타임 부트스트랩 구성을 확인합니다.","Vérifie l’état DNSCrypt local, l’interception DNS Administrateur, la politique fail-closed, l’isolation du DNS système et le bootstrap du runtime actif." +Unable to save {0}: {1},Не удалось сохранить {0}: {1},无法保存 {0}: {1},無法儲存 {0}: {1},{0} を保存できません: {1},{0}을(를) 저장할 수 없습니다: {1},Impossible d’enregistrer {0} : {1} +Unable to save log viewer settings: {0},Не удалось сохранить настройки просмотра журнала: {0},无法保存日志查看器设置: {0},無法儲存記錄檢視器設定: {0},ログ ビューアー設定を保存できません: {0},로그 뷰어 설정을 저장할 수 없습니다: {0},Impossible d’enregistrer les paramètres de l’afficheur de journaux : {0} +"Automatic mode selects a resolver from the signed catalog using the DNSSEC, no-log, unfiltered and address-family settings above.","Автоматический режим выбирает резолвер из подписанного каталога с учётом указанных выше настроек DNSSEC, отсутствия журналирования, отсутствия фильтрации и семейства адресов.",自动模式使用上方的 DNSSEC、无日志、无过滤和地址族设置从签名目录中选择解析器。,自動模式會使用上方的 DNSSEC、無日誌、無過濾及位址家族設定,從簽署目錄中選擇解析器。,自動モードでは、上記の DNSSEC、ログなし、フィルタなし、アドレスファミリー設定に従って署名済みカタログからリゾルバーを選択します。,"자동 모드는 위의 DNSSEC, 무로그, 무필터 및 주소 패밀리 설정에 따라 서명된 카탈로그에서 리졸버를 선택합니다.","Le mode automatique sélectionne un résolveur dans le catalogue signé selon les paramètres DNSSEC, sans journalisation, sans filtrage et de famille d’adresses ci-dessus." +Show DNSCrypt and DoH runtime diagnostics in the log viewer and allow DNS runtime output to be written. Resolver probe diagnostics and RTT details also require Verbose Logging.,Показывать диагностику DNSCrypt и DoH в просмотрщике журнала и разрешать запись вывода DNS-службы. Для диагностики проверок резолверов и данных RTT также требуется подробное журналирование.,在日志查看器中显示 DNSCrypt 和 DoH 运行时诊断,并允许写入 DNS 运行时输出。解析器探测诊断和 RTT 详情还需要启用详细日志。,在日誌檢視器中顯示 DNSCrypt 與 DoH 執行階段診斷,並允許寫入 DNS 執行階段輸出。解析器探測診斷與 RTT 詳細資料還需要啟用詳細記錄。,ログビューアーに DNSCrypt/DoH のランタイム診断を表示し、DNS ランタイム出力の記録を許可します。リゾルバーのプローブ診断と RTT 詳細には詳細ログも必要です。,로그 뷰어에 DNSCrypt 및 DoH 런타임 진단을 표시하고 DNS 런타임 출력 기록을 허용합니다. 리졸버 프로브 진단과 RTT 세부 정보에는 상세 로깅도 필요합니다.,Afficher les diagnostics d’exécution DNSCrypt et DoH dans le journal et autoriser l’écriture de la sortie DNS. Les diagnostics de sondage des résolveurs et les détails RTT nécessitent aussi la journalisation détaillée. +Install update,Установить обновление,安装更新,安裝更新,アップデートをインストール,업데이트 설치,Installer la mise à jour +"Download, verify and install the selected update automatically.","Скачать, проверить и автоматически установить выбранное обновление.",下载、验证并自动安装所选更新。,下載、驗證並自動安裝所選更新。,選択したアップデートをダウンロード、検証して自動的にインストールします。,선택한 업데이트를 다운로드하고 검증한 뒤 자동으로 설치합니다.,"Télécharger, vérifier et installer automatiquement la mise à jour sélectionnée." +Installing update {0}…,Установка обновления {0}…,正在安装更新 {0}…,正在安裝更新 {0}…,アップデート {0} をインストールしています…,업데이트 {0} 설치 중…,Installation de la mise à jour {0}… +Automatic update failed: {0},Не удалось выполнить автоматическое обновление: {0},自动更新失败:{0},自動更新失敗:{0},自動アップデートに失敗しました: {0},자동 업데이트 실패: {0},Échec de la mise à jour automatique : {0} +Update check failed: {0},Не удалось проверить обновления: {0},检查更新失败:{0},檢查更新失敗:{0},アップデートの確認に失敗しました: {0},업데이트 확인 실패: {0},Échec de la vérification des mises à jour : {0} +Automatically install updates,Автоматически устанавливать обновления,自动安装更新,自動安裝更新,アップデートを自動インストール,업데이트 자동 설치,Installer automatiquement les mises à jour +"Automatically download, verify and install a newer release after the application starts.","После запуска автоматически скачивать, проверять и устанавливать новую версию.",应用启动后自动下载、验证并安装新版本。,應用程式啟動後自動下載、驗證並安裝新版本。,アプリ起動後に新しいリリースを自動的にダウンロード、検証してインストールします。,애플리케이션 시작 후 새 릴리스를 자동으로 다운로드하고 검증한 뒤 설치합니다.,"Télécharger, vérifier et installer automatiquement une nouvelle version après le démarrage de l’application." +Local managed routing,Локальная managed-маршрутизация,本地托管路由,本機受控路由,ローカルマネージドルーティング,로컬 관리형 라우팅,Routage géré local +Local GeoSite and ABP/EasyList network rules are always evaluated by the managed C# FilterEngine. The local PAC contains only a minimal proxy funnel and never evaluates filtering rules.,Локальные сетевые правила GeoSite и ABP/EasyList всегда обрабатываются managed-движком FilterEngine на C#. Local PAC содержит только минимальный переход к прокси и никогда не выполняет правила фильтрации.,本地 GeoSite 和 ABP/EasyList 网络规则始终由托管 C# FilterEngine 处理。Local PAC 只包含最小代理转发逻辑,绝不执行过滤规则。,本機 GeoSite 與 ABP/EasyList 網路規則一律由受控 C# FilterEngine 處理。Local PAC 只包含最小 Proxy 導流邏輯,絕不執行篩選規則。,ローカルの GeoSite および ABP/EasyList ネットワークルールは常にマネージド C# FilterEngine で評価されます。Local PAC は最小限のプロキシ転送だけを含み、フィルタールールは実行しません。,로컬 GeoSite 및 ABP/EasyList 네트워크 규칙은 항상 관리형 C# FilterEngine에서 평가됩니다. Local PAC에는 최소 프록시 전달 로직만 있으며 필터 규칙을 실행하지 않습니다.,Les règles réseau GeoSite et ABP/EasyList locales sont toujours évaluées par le FilterEngine C# géré. Le PAC local ne contient qu’un acheminement minimal vers le proxy et n’évalue jamais les règles de filtrage. +Refresh routing diagnostics,Обновить диагностику маршрутизации,刷新路由诊断,重新整理路由診斷,ルーティング診断を更新,라우팅 진단 새로 고침,Actualiser le diagnostic de routage +"Refresh the local managed-routing generation, rule counts and decision counters shown above.","Обновить показанные выше поколение managed routing, количество правил и счётчики решений.",刷新上方显示的本地托管路由代次、规则数量和决策计数器。,重新整理上方顯示的本機受控路由世代、規則數量與決策計數器。,上に表示されたローカルのマネージドルーティング世代、ルール数、判定カウンターを更新します。,"위에 표시된 로컬 관리형 라우팅 세대, 규칙 수 및 결정 카운터를 새로 고칩니다.","Actualise la génération de routage géré locale, le nombre de règles et les compteurs de décisions affichés ci-dessus." +Effective routing mode: {0},Фактический режим маршрутизации: {0},实际路由模式:{0},實際路由模式:{0},実効ルーティングモード: {0},실제 라우팅 모드: {0},Mode de routage effectif : {0} +"Snapshot: {0}, generation {1}, source: {2}","Snapshot: {0}, поколение {1}, источник: {2}",快照:{0},代次 {1},来源:{2},快照:{0},世代 {1},來源:{2},スナップショット: {0}、世代 {1}、ソース: {2},"스냅샷: {0}, 세대 {1}, 소스: {2}","Instantané : {0}, génération {1}, source : {2}" +Rules: {0} default + {1} user; invalid: {2},Правила: {0} базовых + {1} пользовательских; некорректных: {2},规则:{0} 个默认 + {1} 个用户;无效:{2},規則:{0} 個預設 + {1} 個使用者;無效:{2},ルール: 既定 {0} + ユーザー {1}、無効: {2},규칙: 기본 {0} + 사용자 {1}; 잘못된 규칙: {2},Règles : {0} par défaut + {1} utilisateur ; invalides : {2} +Managed decisions in this snapshot: {0} direct / {1} proxy,Решения managed routing в этом snapshot: {0} напрямую / {1} через прокси,此快照中的托管路由决策:{0} 直连 / {1} 代理,此快照中的受控路由決策:{0} 直連 / {1} Proxy,このスナップショットのマネージド判定: Direct {0} / Proxy {1},이 스냅샷의 관리형 결정: Direct {0} / Proxy {1},Décisions gérées dans cet instantané : {0} direct / {1} proxy +Administrator relay: managed routing active ({0} rules),Ретранслятор Administrator Mode: managed routing активен ({0} правил),管理员中继:托管路由已启用({0} 条规则),系統管理員轉送:受控路由已啟用({0} 條規則),管理者リレー: マネージドルーティング有効({0} ルール),관리자 릴레이: 관리형 라우팅 활성({0}개 규칙),Relais Administrateur : routage géré actif ({0} règles) +Administrator relay: managed routing inactive,Ретранслятор Administrator Mode: managed routing неактивен,管理员中继:托管路由未启用,系統管理員轉送:受控路由未啟用,管理者リレー: マネージドルーティング無効,관리자 릴레이: 관리형 라우팅 비활성,Relais Administrateur : routage géré inactif +Managed routing,Управляемая маршрутизация,托管路由,受控路由,マネージドルーティング,관리형 라우팅,Routage géré +LocalRules,Локальные правила,本地规则,本機規則,ローカルルール,로컬 규칙,Règles locales +ProxyAllBootstrap,Bootstrap: всё через прокси,引导:全部代理,啟動:全部 Proxy,ブートストラップ: 全て Proxy,부트스트랩: 모두 프록시,Bootstrap : tout via proxy +GeoSite + user rules,GeoSite + пользовательские правила,GeoSite + 用户规则,GeoSite + 使用者規則,GeoSite + ユーザールール,GeoSite + 사용자 규칙,GeoSite + règles utilisateur +system proxy is disabled,системный прокси отключён,系统代理已禁用,系統 Proxy 已停用,システムプロキシが無効,시스템 프록시가 비활성화됨,le proxy système est désactivé +global mode already routes every request through Shadowsocks,Global Mode уже направляет все запросы через Shadowsocks,全局模式已将所有请求通过 Shadowsocks 路由,全域模式已將所有請求透過 Shadowsocks 路由,グローバルモードは既に全リクエストを Shadowsocks 経由にしています,글로벌 모드가 이미 모든 요청을 Shadowsocks로 라우팅합니다,le mode global route déjà toutes les requêtes via Shadowsocks +online PAC is the selected external routing mode,выбран внешний режим Online PAC,已选择外部 Online PAC 路由模式,已選擇外部 Online PAC 路由模式,外部 Online PAC ルーティングモードが選択されています,외부 Online PAC 라우팅 모드가 선택됨,le mode de routage PAC en ligne externe est sélectionné +GeoSite is not ready; matching Local PAC proxy-all bootstrap,GeoSite не готов; используется совместимый Local PAC bootstrap «всё через прокси»,GeoSite 尚未就绪;使用与 Local PAC 一致的全部代理引导,GeoSite 尚未就緒;使用與 Local PAC 一致的全部 Proxy 啟動模式,GeoSite 未準備のため Local PAC と同じ全 Proxy ブートストラップを使用,GeoSite가 준비되지 않아 Local PAC와 동일한 전체 프록시 부트스트랩 사용,GeoSite n’est pas prêt ; bootstrap tout-proxy identique au PAC local +not initialized,не инициализировано,未初始化,尚未初始化,未初期化,초기화되지 않음,non initialisé +"#r38 plugin import, DNSCrypt progress and local PAC user rules",,,,,, +Required plugin is not installed,Требуемый плагин не установлен,所需插件尚未安装,所需外掛尚未安裝,必要なプラグインがインストールされていません,필요한 플러그인이 설치되어 있지 않습니다,Le plugin requis n’est pas installé +This ss:// configuration requires {0}. The plugin is available in the built-in catalog but is not installed.,"Для этой конфигурации ss:// требуется {0}. Плагин доступен во встроенном каталоге, но не установлен.",此 ss:// 配置需要 {0}。该插件可在内置目录中获取,但尚未安装。,此 ss:// 設定需要 {0}。此外掛可在內建目錄中取得,但尚未安裝。,この ss:// 設定には {0} が必要です。プラグインは内蔵カタログにありますが、まだインストールされていません。,이 ss:// 구성에는 {0} 플러그인이 필요합니다. 기본 제공 카탈로그에 있지만 아직 설치되지 않았습니다.,Cette configuration ss:// nécessite {0}. Le plugin est disponible dans le catalogue intégré mais n’est pas installé. +Import without plugin,Импортировать без плагина,不安装插件直接导入,不安裝外掛直接匯入,プラグインなしでインポート,플러그인 없이 가져오기,Importer sans le plugin +Installing required plugin: {0},Установка требуемого плагина: {0},正在安装所需插件:{0},正在安裝所需外掛:{0},必要なプラグインをインストール中: {0},필요한 플러그인 설치 중: {0},Installation du plugin requis : {0} +Failed to install required plugin {0}: {1},Не удалось установить требуемый плагин {0}: {1},无法安装所需插件 {0}:{1},無法安裝所需外掛 {0}:{1},必要なプラグイン {0} のインストールに失敗しました: {1},필요한 플러그인 {0} 설치 실패: {1},Échec de l’installation du plugin requis {0} : {1} +Required plugin installation completed.,Установка требуемого плагина завершена.,所需插件安装完成。,所需外掛安裝完成。,必要なプラグインのインストールが完了しました。,필요한 플러그인 설치가 완료되었습니다.,Installation du plugin requis terminée. +Local user rules,Локальные пользовательские правила,本地用户规则,本機使用者規則,ローカルユーザールール,로컬 사용자 규칙,Règles utilisateur locales +Edit user-rule.txt to add your own ABP/EasyList-style network routing rules. User rules are evaluated before GeoSite defaults; normal rules use the proxy and @@ exception rules go direct. Changes are watched and applied automatically.,"Отредактируйте user-rule.txt, чтобы добавить собственные сетевые правила маршрутизации в стиле ABP/EasyList. Пользовательские правила проверяются раньше правил GeoSite; обычные правила используют прокси, а правила-исключения @@ направляют трафик напрямую. Изменения отслеживаются и применяются автоматически.",编辑 user-rule.txt 可添加自定义 ABP/EasyList 风格网络路由规则。用户规则优先于 GeoSite 默认规则;普通规则走代理,@@ 例外规则直连。更改会被监视并自动应用。,編輯 user-rule.txt 可加入自訂 ABP/EasyList 風格網路路由規則。使用者規則優先於 GeoSite 預設規則;一般規則走代理,@@ 例外規則直連。變更會被監視並自動套用。,user-rule.txt を編集して独自の ABP/EasyList 形式のネットワークルーティングルールを追加できます。ユーザールールは GeoSite の既定ルールより先に評価され、通常ルールはプロキシ、@@ 例外ルールは直接接続になります。変更は監視され自動適用されます。,"user-rule.txt을 편집하여 ABP/EasyList 형식의 사용자 네트워크 라우팅 규칙을 추가할 수 있습니다. 사용자 규칙은 GeoSite 기본 규칙보다 먼저 평가되며 일반 규칙은 프록시, @@ 예외 규칙은 직접 연결을 사용합니다. 변경 사항은 감지되어 자동 적용됩니다.",Modifiez user-rule.txt pour ajouter vos propres règles de routage réseau de style ABP/EasyList. Les règles utilisateur sont évaluées avant les règles GeoSite ; les règles normales utilisent le proxy et les exceptions @@ passent en direct. Les modifications sont surveillées et appliquées automatiquement. +Open user-rule.txt,Открыть user-rule.txt,打开 user-rule.txt,開啟 user-rule.txt,user-rule.txt を開く,user-rule.txt 열기,Ouvrir user-rule.txt +Create or open the active user-rule.txt file in your default text editor.,Создать или открыть активный файл user-rule.txt в текстовом редакторе по умолчанию.,在默认文本编辑器中创建或打开当前 user-rule.txt 文件。,在預設文字編輯器中建立或開啟目前的 user-rule.txt 檔案。,現在の user-rule.txt を既定のテキストエディターで作成または開きます。,기본 텍스트 편집기에서 현재 user-rule.txt 파일을 만들거나 엽니다.,Créer ou ouvrir le fichier user-rule.txt actif dans l’éditeur de texte par défaut. +Installing DNSCrypt Proxy,Установка DNSCrypt Proxy,正在安装 DNSCrypt Proxy,正在安裝 DNSCrypt Proxy,DNSCrypt Proxy をインストール中,DNSCrypt Proxy 설치 중,Installation de DNSCrypt Proxy +Activating DNSCrypt,Активация DNSCrypt,正在激活 DNSCrypt,正在啟用 DNSCrypt,DNSCrypt を有効化中,DNSCrypt 활성화 중,Activation de DNSCrypt +Updating DNSCrypt Proxy,Обновление DNSCrypt Proxy,正在更新 DNSCrypt Proxy,正在更新 DNSCrypt Proxy,DNSCrypt Proxy を更新中,DNSCrypt Proxy 업데이트 중,Mise à jour de DNSCrypt Proxy +Removing DNSCrypt Proxy,Удаление DNSCrypt Proxy,正在删除 DNSCrypt Proxy,正在移除 DNSCrypt Proxy,DNSCrypt Proxy を削除中,DNSCrypt Proxy 제거 중,Suppression de DNSCrypt Proxy +Download the latest Windows x64 release archive and replace the installed selected plugin.,Скачать последний архив релиза Windows x64 и заменить установленный выбранный плагин.,下载最新的 Windows x64 发布归档并替换已安装的所选插件。,下載最新的 Windows x64 發行封存檔並取代已安裝的所選外掛程式。,最新の Windows x64 リリースアーカイブをダウンロードし、インストール済みの選択したプラグインを置き換えます。,최신 Windows x64 릴리스 아카이브를 다운로드하여 설치된 선택 플러그인을 교체합니다.,Télécharger la dernière archive Windows x64 et remplacer le plugin sélectionné déjà installé. +Save GeoSite source changes before downloading or refreshing data.,Сохраните изменения источников GeoSite перед загрузкой или обновлением данных.,下载或刷新数据前,请先保存 GeoSite 源更改。,下載或重新整理資料前,請先儲存 GeoSite 來源變更。,データをダウンロードまたは更新する前に GeoSite ソースの変更を保存してください。,데이터를 다운로드하거나 새로 고치기 전에 GeoSite 소스 변경 사항을 저장하세요.,Enregistrez les modifications des sources GeoSite avant de télécharger ou d’actualiser les données. +#r39 UI localization and contextual visibility,,,,,, +"Open application appearance, startup and storage settings.","Открыть настройки оформления, автозапуска и хранилища приложения.",打开应用外观、启动和存储设置。,開啟應用程式外觀、啟動與儲存設定。,アプリの外観、起動、ストレージ設定を開きます。,"앱의 모양, 시작 및 저장소 설정을 엽니다.","Ouvrir les paramètres d’apparence, de démarrage et de stockage de l’application." +Edit Local User Rules,Изменить локальные пользовательские правила,编辑本地用户规则,編輯本機使用者規則,ローカルユーザールールを編集,로컬 사용자 규칙 편집,Modifier les règles utilisateur locales +Select a GeoSite source before moving or removing it.,"Выберите источник GeoSite, прежде чем перемещать или удалять его.",移动或删除前,请先选择一个 GeoSite 源。,移動或移除前,請先選擇一個 GeoSite 來源。,移動または削除する前に GeoSite ソースを選択してください。,이동하거나 제거하기 전에 GeoSite 소스를 선택하세요.,Sélectionnez une source GeoSite avant de la déplacer ou de la supprimer. +Enter an absolute HTTP or HTTPS PAC URL.,Введите абсолютный PAC URL с протоколом HTTP или HTTPS.,请输入绝对 HTTP 或 HTTPS PAC URL。,請輸入絕對 HTTP 或 HTTPS PAC URL。,HTTP または HTTPS の絶対 PAC URL を入力してください。,절대 HTTP 또는 HTTPS PAC URL을 입력하세요.,Saisissez une URL PAC HTTP ou HTTPS absolue. +"Select a server to edit, duplicate, share, move, or delete it.","Выберите сервер, чтобы изменить, дублировать, поделиться, переместить или удалить его.",选择服务器以编辑、复制、共享、移动或删除。,選擇伺服器以編輯、複製、分享、移動或刪除。,編集、複製、共有、移動、削除するサーバーを選択します。,"편집, 복제, 공유, 이동 또는 삭제할 서버를 선택하세요.","Sélectionnez un serveur pour le modifier, le dupliquer, le partager, le déplacer ou le supprimer." +Not tested,Не проверено,未检查,未檢查,未確認,확인 안 됨,Non testé +Registered,Зарегистрировано,已注册,已註冊,登録済み,등록됨,Enregistré +Registration failed,Регистрация не удалась,注册失败,註冊失敗,登録に失敗,등록 실패,Échec de l’enregistrement +"Select a server to share, copy, or delete it.","Выберите сервер, чтобы поделиться им, скопировать ссылку или удалить его.",选择服务器以共享、复制链接或删除。,選擇伺服器以分享、複製連結或刪除。,共有、リンクのコピー、削除を行うサーバーを選択します。,공유하거나 링크를 복사하거나 삭제할 서버를 선택하세요.,"Sélectionnez un serveur pour le partager, copier son lien ou le supprimer." +The selected server's ss:// URL. Use Copy to place it on the clipboard.,"Ссылка ss:// выбранного сервера. Нажмите «Копировать», чтобы поместить её в буфер обмена.",所选服务器的 ss:// URL。使用“复制”将其放入剪贴板。,所選伺服器的 ss:// URL。使用「複製」將其放入剪貼簿。,選択したサーバーの ss:// URL です。「コピー」でクリップボードにコピーします。,선택한 서버의 ss:// URL입니다. '복사'를 사용하여 클립보드에 넣습니다.,URL ss:// du serveur sélectionné. Utilisez Copier pour la placer dans le Presse-papiers. +Shows the resolvers currently chosen by automatic DNSCrypt selection.,"Показывает резолверы, которые сейчас выбраны автоматическим режимом DNSCrypt.",显示 DNSCrypt 自动选择当前选用的解析器。,顯示 DNSCrypt 自動選擇目前選用的解析器。,DNSCrypt の自動選択で現在選ばれているリゾルバーを表示します。,DNSCrypt 자동 선택이 현재 선택한 리졸버를 표시합니다.,Affiche les résolveurs actuellement choisis par la sélection automatique DNSCrypt. +Release notes for the selected available update.,Примечания к выбранному доступному обновлению.,所选可用更新的发行说明。,所選可用更新的發行說明。,選択された利用可能な更新のリリースノートです。,선택한 사용 가능한 업데이트의 릴리스 정보입니다.,Notes de version de la mise à jour disponible sélectionnée. +Timeout (Sec),Тайм-аут (сек.),超时(秒),逾時(秒),タイムアウト(秒),시간 제한(초),Délai (s) +Toggle system proxy,Переключить системный прокси,切换系统代理,切換系統代理,システムプロキシを切り替え,시스템 프록시 전환,Basculer le proxy système +Switch Global / PAC,Переключить режим «Глобальный / PAC»,切换“全局 / PAC”模式,切換「全域 / PAC」模式,「グローバル / PAC」を切り替え,'글로벌 / PAC' 모드 전환,Basculer Global / PAC +Allow clients from LAN,Разрешить подключения из локальной сети,允许局域网客户端连接,允許區域網路用戶端連線,LAN クライアントからの接続を許可,LAN 클라이언트 연결 허용,Autoriser les clients du réseau local +Open logs window,Открыть окно журналов,打开日志窗口,開啟記錄視窗,ログウィンドウを開く,로그 창 열기,Ouvrir la fenêtre des journaux +Switch to previous server,Переключиться на предыдущий сервер,切换到上一个服务器,切換到上一個伺服器,前のサーバーに切り替える,이전 서버로 전환,Passer au serveur précédent +Switch to next server,Переключиться на следующий сервер,切换到下一个服务器,切換到下一個伺服器,次のサーバーに切り替える,다음 서버로 전환,Passer au serveur suivant +Save the online PAC URL used when Online PAC mode is enabled.,"Сохранить URL Online PAC, используемый при включённом режиме Online PAC.",保存启用 Online PAC 模式时使用的 PAC URL。,儲存啟用 Online PAC 模式時使用的 PAC URL。,Online PAC モードで使用する PAC URL を保存します。,Online PAC 모드가 활성화될 때 사용할 PAC URL을 저장합니다.,Enregistrer l’URL PAC utilisée lorsque le mode PAC en ligne est activé. +Address,Адрес,地址,地址,アドレス,주소,Adresse +Port,Порт,端口,連接埠,ポート,포트,Port +Timeout,Тайм-аут,超时,逾時,タイムアウト,시간 제한,Délai +Username,Имя пользователя,用户名,使用者名稱,ユーザー名,사용자 이름,Nom d’utilisateur +"DNSCrypt Proxy could not become ready. Open Logs to see the upstream error, then try again. The previous DNS mode was kept.","DNSCrypt Proxy не удалось перейти в готовое состояние. Откройте «Журналы», чтобы увидеть ошибку upstream, затем повторите попытку. Предыдущий режим DNS сохранён.",DNSCrypt Proxy 无法进入就绪状态。请打开“日志”查看上游错误,然后重试。已保留之前的 DNS 模式。,DNSCrypt Proxy 無法進入就緒狀態。請開啟「日誌」查看上游錯誤,然後再試一次。已保留先前的 DNS 模式。,DNSCrypt Proxy を準備完了状態にできませんでした。「ログ」を開いてアップストリームのエラーを確認し、もう一度お試しください。以前の DNS モードは保持されています。,DNSCrypt Proxy가 준비 상태가 되지 못했습니다. 로그를 열어 업스트림 오류를 확인한 뒤 다시 시도하세요. 이전 DNS 모드는 유지되었습니다.,"DNSCrypt Proxy n’a pas pu devenir opérationnel. Ouvrez les journaux pour consulter l’erreur en amont, puis réessayez. Le mode DNS précédent a été conservé." +"The active DNSCrypt runtime never uses plaintext or system DNS. Resolver-catalog refresh uses Cloudflare DoH through Shadowsocks, with Google DoH as fallback, and publishes the catalog only after Minisign verification.","Активный DNSCrypt runtime никогда не использует незашифрованный или системный DNS. Каталог резолверов обновляется через Cloudflare DoH по Shadowsocks, с резервным Google DoH, и публикуется только после проверки Minisign.",活动 DNSCrypt 运行时绝不使用明文或系统 DNS。解析器目录刷新通过 Shadowsocks 使用 Cloudflare DoH,并以 Google DoH 作为备用;目录仅在通过 Minisign 验证后发布。,作用中的 DNSCrypt 執行階段絕不使用明文或系統 DNS。解析器目錄重新整理透過 Shadowsocks 使用 Cloudflare DoH,並以 Google DoH 作為備援;目錄僅在通過 Minisign 驗證後發布。,アクティブな DNSCrypt ランタイムは平文 DNS やシステム DNS を使用しません。リゾルバーカタログは Shadowsocks 経由の Cloudflare DoH で更新し、Google DoH をフォールバックとして使用し、Minisign 検証に成功した後にのみ公開します。,"활성 DNSCrypt 런타임은 평문 DNS 또는 시스템 DNS를 사용하지 않습니다. 리졸버 카탈로그는 Shadowsocks를 통한 Cloudflare DoH로 갱신하고 Google DoH를 예비 경로로 사용하며, Minisign 검증에 성공한 뒤에만 게시합니다.","Le runtime DNSCrypt actif n’utilise jamais le DNS en clair ni le DNS système. Le catalogue des résolveurs est actualisé via Cloudflare DoH à travers Shadowsocks, avec Google DoH en secours, puis publié uniquement après vérification Minisign." +"DNSCrypt could not refresh the signed resolver catalog through Cloudflare DoH over Shadowsocks or the Google DoH fallback. Verify that a Shadowsocks server is connected and HTTPS through the proxy works, then try again. The previous DNS mode was kept.","DNSCrypt не удалось обновить подписанный каталог резолверов через Cloudflare DoH по Shadowsocks или резервный Google DoH. Проверьте, что сервер Shadowsocks подключён и HTTPS через прокси работает, затем повторите попытку. Предыдущий режим DNS сохранён.",DNSCrypt 无法通过 Shadowsocks 上的 Cloudflare DoH 或备用 Google DoH 刷新已签名的解析器目录。请确认 Shadowsocks 服务器已连接且通过代理的 HTTPS 可用,然后重试。已保留之前的 DNS 模式。,DNSCrypt 無法透過 Shadowsocks 上的 Cloudflare DoH 或備援 Google DoH 重新整理已簽署的解析器目錄。請確認 Shadowsocks 伺服器已連線且透過 Proxy 的 HTTPS 可用,然後再試一次。已保留先前的 DNS 模式。,DNSCrypt は Shadowsocks 経由の Cloudflare DoH またはフォールバックの Google DoH で署名済みリゾルバーカタログを更新できませんでした。Shadowsocks サーバーが接続され、プロキシ経由の HTTPS が利用できることを確認してから再試行してください。以前の DNS モードは保持されています。,DNSCrypt가 Shadowsocks를 통한 Cloudflare DoH 또는 예비 Google DoH로 서명된 리졸버 카탈로그를 갱신하지 못했습니다. Shadowsocks 서버가 연결되어 있고 프록시를 통한 HTTPS가 작동하는지 확인한 후 다시 시도하세요. 이전 DNS 모드는 유지되었습니다.,"DNSCrypt n’a pas pu actualiser le catalogue signé des résolveurs via Cloudflare DoH à travers Shadowsocks ni via Google DoH de secours. Vérifiez qu’un serveur Shadowsocks est connecté et que HTTPS fonctionne via le proxy, puis réessayez. Le mode DNS précédent a été conservé." +Preparing signed DNSCrypt resolver catalog through DoH over Shadowsocks,Подготовка подписанного каталога DNSCrypt-резолверов через DoH по Shadowsocks,正在通过 Shadowsocks 上的 DoH 准备已签名的 DNSCrypt 解析器目录,正在透過 Shadowsocks 上的 DoH 準備已簽署的 DNSCrypt 解析器目錄,Shadowsocks 経由の DoH で署名済み DNSCrypt リゾルバーカタログを準備しています,Shadowsocks를 통한 DoH로 서명된 DNSCrypt 리졸버 카탈로그를 준비하는 중,Préparation du catalogue signé des résolveurs DNSCrypt via DoH à travers Shadowsocks +"DNSCrypt could not reach GitHub through Cloudflare DoH over Shadowsocks or the Google DoH fallback. Verify that a Shadowsocks server is connected and HTTPS through the proxy works, then try again. No DNSCrypt component changes were applied.","DNSCrypt не удалось подключиться к GitHub через Cloudflare DoH поверх Shadowsocks или резервный Google DoH. Убедитесь, что сервер Shadowsocks подключён и HTTPS через прокси работает, затем повторите попытку. Компонент DNSCrypt не изменён.",DNSCrypt 无法通过 Shadowsocks 上的 Cloudflare DoH 或备用 Google DoH 访问 GitHub。请确认 Shadowsocks 服务器已连接且代理中的 HTTPS 可用,然后重试。DNSCrypt 组件未发生更改。,DNSCrypt 無法透過 Shadowsocks 上的 Cloudflare DoH 或備用 Google DoH 存取 GitHub。請確認 Shadowsocks 伺服器已連線且代理中的 HTTPS 可用,然後重試。DNSCrypt 元件未發生變更。,DNSCrypt は Shadowsocks 経由の Cloudflare DoH または予備の Google DoH を使用して GitHub に接続できませんでした。Shadowsocks サーバーが接続済みで、プロキシ経由の HTTPS が利用できることを確認してから再試行してください。DNSCrypt コンポーネントには変更を加えていません。,DNSCrypt가 Shadowsocks를 통한 Cloudflare DoH 또는 예비 Google DoH를 사용해 GitHub에 연결하지 못했습니다. Shadowsocks 서버가 연결되어 있고 프록시를 통한 HTTPS가 작동하는지 확인한 후 다시 시도하세요. DNSCrypt 구성 요소는 변경되지 않았습니다.,"DNSCrypt n’a pas pu joindre GitHub via Cloudflare DoH sur Shadowsocks ni via le secours Google DoH. Vérifiez qu’un serveur Shadowsocks est connecté et que HTTPS fonctionne via le proxy, puis réessayez. Le composant DNSCrypt n’a pas été modifié." +No servers configured. Select Add to create one.,"Серверы не настроены. Нажмите «Добавить», чтобы создать сервер.",未配置服务器。选择“添加”以创建服务器。,尚未設定伺服器。選擇「新增」以建立伺服器。,サーバーが設定されていません。「追加」を選択して作成してください。,구성된 서버가 없습니다. 서버를 만들려면 추가를 선택하세요.,Aucun serveur n’est configuré. Sélectionnez Ajouter pour en créer un. +Add a configured Shadowsocks server before using DNS or PAC settings.,Добавьте настроенный сервер Shadowsocks перед использованием настроек DNS или PAC.,使用 DNS 或 PAC 设置前,请先添加已配置的 Shadowsocks 服务器。,使用 DNS 或 PAC 設定前,請先新增已設定的 Shadowsocks 伺服器。,DNS または PAC 設定を使用する前に、設定済みの Shadowsocks サーバーを追加してください。,DNS 또는 PAC 설정을 사용하기 전에 구성된 Shadowsocks 서버를 추가하세요.,Ajoutez un serveur Shadowsocks configuré avant d’utiliser les paramètres DNS ou PAC. +Add a configured Shadowsocks server before enabling the Windows system proxy.,Добавьте настроенный сервер Shadowsocks перед включением системного прокси Windows.,启用 Windows 系统代理前,请先添加已配置的 Shadowsocks 服务器。,啟用 Windows 系統 Proxy 前,請先新增已設定的 Shadowsocks 伺服器。,Windows システムプロキシを有効にする前に、設定済みの Shadowsocks サーバーを追加してください。,Windows 시스템 프록시를 사용하기 전에 구성된 Shadowsocks 서버를 추가하세요.,Ajoutez un serveur Shadowsocks configuré avant d’activer le proxy système Windows. +Applying traffic capture mode…,Применение режима перехвата трафика…,正在应用流量捕获模式…,正在套用流量擷取模式…,トラフィックキャプチャモードを適用しています…,트래픽 캡처 모드를 적용하는 중…,Application du mode de capture du trafic… +Waiting for the update operation to finish…,Ожидание завершения операции обновления…,正在等待更新操作完成…,正在等待更新作業完成…,更新処理の完了を待っています…,업데이트 작업이 완료되기를 기다리는 중…,Attente de la fin de l’opération de mise à jour… diff --git a/Shadowsocks.Core/Data/user-rule.txt b/Shadowsocks.Core/Data/user-rule.txt new file mode 100644 index 000000000..fe41d9d1c --- /dev/null +++ b/Shadowsocks.Core/Data/user-rule.txt @@ -0,0 +1,5 @@ +! Put user routing rules line by line in this file. +! Rules use the supported ABP/EasyList network-filter syntax. +! Normal matching rules route through the proxy; @@ exception rules go DIRECT. +! User rules are evaluated before GeoSite defaults and changes are applied automatically. +! See https://adblockplus.org/en/filter-cheatsheet diff --git a/Shadowsocks.Core/EmbeddedResources.cs b/Shadowsocks.Core/EmbeddedResources.cs new file mode 100644 index 000000000..df6e57a60 --- /dev/null +++ b/Shadowsocks.Core/EmbeddedResources.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; + +namespace Shadowsocks.Core +{ + public static class EmbeddedResources + { + public static string I18nCsv => ReadText("Shadowsocks.Core.Data.i18n.csv", Encoding.UTF8); + public static string AppSettingsJson => ReadText("Shadowsocks.Core.appsettings.json", Encoding.UTF8); + public static string UserRule => ReadText("Shadowsocks.Core.Data.user-rule.txt", Encoding.UTF8); + public static string ProductLicense => ReadRequiredText("Shadowsocks.Core.LICENSE.txt", Encoding.UTF8); + public static string ThirdPartyNotices => ReadRequiredText("Shadowsocks.Core.THIRD-PARTY-NOTICES.txt", Encoding.UTF8); + + public static string LocalizedProductLicense(CultureInfo culture) + => LegalDocumentLocalizer.CreateProductLicense(ProductLicense, culture); + + public static string LocalizedThirdPartyNotices(CultureInfo culture) + => LegalDocumentLocalizer.CreateThirdPartyNotices(ThirdPartyNotices, culture); + + private static string ReadText(string resourceName, Encoding encoding) + { + Assembly assembly = typeof(EmbeddedResources).Assembly; + using Stream stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Embedded resource '{resourceName}' was not found."); + using StreamReader reader = new(stream, encoding, true); + return reader.ReadToEnd(); + } + + private static string ReadRequiredText(string resourceName, Encoding encoding) + { + string text = ReadText(resourceName, encoding); + if (string.IsNullOrWhiteSpace(text)) + { + throw new InvalidOperationException($"Embedded resource '{resourceName}' is empty."); + } + + return text; + } + } +} diff --git a/Shadowsocks.Core/Encryption/AEAD/AEADBclEncryptor.cs b/Shadowsocks.Core/Encryption/AEAD/AEADBclEncryptor.cs new file mode 100644 index 000000000..6622c203a --- /dev/null +++ b/Shadowsocks.Core/Encryption/AEAD/AEADBclEncryptor.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Shadowsocks.Encryption.Exception; + +namespace Shadowsocks.Encryption.AEAD +{ + public sealed class AEADBclEncryptor : AEADEncryptor + { + private const int CIPHER_AES_GCM = 1; + private const int CIPHER_CHACHA20_POLY1305 = 2; + + private static readonly Dictionary CipherTypes = new Dictionary + { + {"aes-128-gcm", new EncryptorInfo(16, 16, 12, 16, CIPHER_AES_GCM)}, + {"aes-192-gcm", new EncryptorInfo(24, 24, 12, 16, CIPHER_AES_GCM)}, + {"aes-256-gcm", new EncryptorInfo(32, 32, 12, 16, CIPHER_AES_GCM)}, + {"chacha20-ietf-poly1305", new EncryptorInfo(32, 32, 12, 16, CIPHER_CHACHA20_POLY1305)}, + }; + + private byte[] _encryptSubkey; + private byte[] _decryptSubkey; + private AesGcm _encryptAes; + private AesGcm _decryptAes; + private ChaCha20Poly1305 _encryptChaCha; + private ChaCha20Poly1305 _decryptChaCha; + private bool _disposed; + + public AEADBclEncryptor(string method, string password) + : base(method, password) + { + _encryptSubkey = new byte[KeyLength]; + _decryptSubkey = new byte[KeyLength]; + } + + public static List SupportedCiphers() + { + return new List(CipherTypes.Keys); + } + + protected override IReadOnlyDictionary GetCiphers() + { + return CipherTypes; + } + + public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) + { + base.InitCipher(salt, isEncrypt, isUdp); + + byte[] subkey = isEncrypt ? _encryptSubkey : _decryptSubkey; + DeriveSessionKey(isEncrypt ? EncryptSalt : DecryptSalt, MasterKey, subkey); + + switch (CipherType) + { + case CIPHER_AES_GCM: + if (isEncrypt) + { + _encryptAes?.Dispose(); + _encryptAes = new AesGcm(subkey, TagLength); + } + else + { + _decryptAes?.Dispose(); + _decryptAes = new AesGcm(subkey, TagLength); + } + break; + + case CIPHER_CHACHA20_POLY1305: + if (!ChaCha20Poly1305.IsSupported) + { + throw new PlatformNotSupportedException( + "ChaCha20-Poly1305 is not supported by this Windows version."); + } + + if (isEncrypt) + { + _encryptChaCha?.Dispose(); + _encryptChaCha = new ChaCha20Poly1305(subkey); + } + else + { + _decryptChaCha?.Dispose(); + _decryptChaCha = new ChaCha20Poly1305(subkey); + } + break; + + default: + throw new NotSupportedException($"Encryption method '{MethodName}' is not supported."); + } + } + + public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) + { + int plainLength = checked((int)plen); + if (ciphertext.Length < plainLength + TagLength) + { + throw new ArgumentException("Ciphertext buffer is too small.", nameof(ciphertext)); + } + + ReadOnlySpan plain = plaintext.AsSpan(0, plainLength); + Span encrypted = ciphertext.AsSpan(0, plainLength); + Span tag = ciphertext.AsSpan(plainLength, TagLength); + + try + { + switch (CipherType) + { + case CIPHER_AES_GCM: + if (_encryptAes == null) + throw new InvalidOperationException("Encryption cipher is not initialized."); + _encryptAes.Encrypt(EncryptNonce, plain, encrypted, tag); + break; + + case CIPHER_CHACHA20_POLY1305: + if (_encryptChaCha == null) + throw new InvalidOperationException("Encryption cipher is not initialized."); + _encryptChaCha.Encrypt(EncryptNonce, plain, encrypted, tag); + break; + + default: + throw new NotSupportedException($"Encryption method '{MethodName}' is not supported."); + } + } + catch (CryptographicException e) + { + throw new CryptoErrorException("AEAD encryption failed.", e); + } + + clen = plen + (uint)TagLength; + } + + public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) + { + int cipherLength = checked((int)clen); + if (cipherLength < TagLength) + { + throw new CryptoErrorException("AEAD ciphertext is shorter than the authentication tag."); + } + + int plainLength = cipherLength - TagLength; + if (plaintext.Length < plainLength) + { + throw new ArgumentException("Plaintext buffer is too small.", nameof(plaintext)); + } + + ReadOnlySpan encrypted = ciphertext.AsSpan(0, plainLength); + ReadOnlySpan tag = ciphertext.AsSpan(plainLength, TagLength); + Span plain = plaintext.AsSpan(0, plainLength); + + try + { + switch (CipherType) + { + case CIPHER_AES_GCM: + if (_decryptAes == null) + throw new InvalidOperationException("Decryption cipher is not initialized."); + _decryptAes.Decrypt(DecryptNonce, encrypted, tag, plain); + break; + + case CIPHER_CHACHA20_POLY1305: + if (_decryptChaCha == null) + throw new InvalidOperationException("Decryption cipher is not initialized."); + _decryptChaCha.Decrypt(DecryptNonce, encrypted, tag, plain); + break; + + default: + throw new NotSupportedException($"Encryption method '{MethodName}' is not supported."); + } + } + catch (CryptographicException e) + { + throw new CryptoErrorException("AEAD authentication or decryption failed.", e); + } + + plen = (uint)plainLength; + } + + public override void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _encryptAes?.Dispose(); + _decryptAes?.Dispose(); + _encryptChaCha?.Dispose(); + _decryptChaCha?.Dispose(); + _encryptAes = null; + _decryptAes = null; + _encryptChaCha = null; + _decryptChaCha = null; + + if (_encryptSubkey != null) + CryptographicOperations.ZeroMemory(_encryptSubkey); + if (_decryptSubkey != null) + CryptographicOperations.ZeroMemory(_decryptSubkey); + if (MasterKey != null) + CryptographicOperations.ZeroMemory(MasterKey); + } + } +} diff --git a/Shadowsocks.Core/Encryption/AEAD/AEADEncryptor.cs b/Shadowsocks.Core/Encryption/AEAD/AEADEncryptor.cs new file mode 100644 index 000000000..80c1e0912 --- /dev/null +++ b/Shadowsocks.Core/Encryption/AEAD/AEADEncryptor.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using NLog; +using Shadowsocks.Controller; +using Shadowsocks.Encryption.CircularBuffer; +using Shadowsocks.Controller.Service; +using Shadowsocks.Encryption.Exception; + +namespace Shadowsocks.Encryption.AEAD +{ + public abstract class AEADEncryptor + : EncryptorBase + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + // We are using the same SaltLength and KeyLength + private const string Info = "ss-subkey"; + private static readonly byte[] InfoBytes = Encoding.ASCII.GetBytes(Info); + + // for UDP only + private static readonly byte[] s_udpTempBuffer = new byte[65536]; + + // every connection should create its own buffer + private readonly ByteCircularBuffer _encCircularBuffer = new(MaxInputSize * 2); + private readonly ByteCircularBuffer _decCircularBuffer = new(MaxInputSize * 2); + + public const int ChunkLengthBytes = 2; + public const uint ChunkLengthMask = 0x3FFFu; + + protected string MethodName { get; private set; } + protected int CipherType { get; private set; } + // internal name in the crypto library + protected byte[] MasterKey { get; private set; } + protected int KeyLength { get; private set; } + protected int SaltLength { get; private set; } + protected int TagLength { get; private set; } + protected int NonceLength { get; private set; } + + protected byte[] EncryptSalt { get; private set; } + protected byte[] DecryptSalt { get; private set; } + + private readonly object _nonceIncrementLock = new(); + protected byte[] EncryptNonce { get; private set; } + protected byte[] DecryptNonce { get; private set; } + // Is first packet + private bool _decryptSaltReceived; + private bool _encryptSaltSent; + + // Is first chunk(tcp request) + private bool _tcpRequestSent; + + public AEADEncryptor(string method, string password) + : base(method, password) + { + InitEncryptorInfo(method); + InitKey(password); + // Initialize all-zero nonce for each connection + EncryptNonce = new byte[NonceLength]; + DecryptNonce = new byte[NonceLength]; + } + + protected abstract IReadOnlyDictionary GetCiphers(); + + protected void InitEncryptorInfo(string method) + { + method = method.ToLowerInvariant(); + MethodName = method; + IReadOnlyDictionary ciphers = GetCiphers(); + EncryptorInfo cipherInfo = ciphers[MethodName]; + CipherType = cipherInfo.Type; + if (CipherType == 0) + { + throw new NotSupportedException($"Encryption method '{MethodName}' is not supported."); + } + KeyLength = cipherInfo.KeySize; + SaltLength = cipherInfo.SaltSize; + TagLength = cipherInfo.TagSize; + NonceLength = cipherInfo.NonceSize; + } + + protected void InitKey(string password) + { + byte[] passbuf = Encoding.UTF8.GetBytes(password); + // The master key belongs to this encryptor instance. Keeping it per-instance + // prevents concurrent connections to servers with different passwords from + // overwriting each other's key material. + MasterKey = new byte[KeyLength]; + DeriveKey(passbuf, MasterKey, KeyLength); + } + + public static void DeriveKey(byte[] password, byte[] key, int keylen) + { + byte[] result = new byte[password.Length + Md5Length]; + int i = 0; + byte[] md5sum = null; + while (i < keylen) + { + if (i == 0) + { + // Shadowsocks EVP_BytesToKey compatibility requires MD5; changing it breaks the protocol. +#pragma warning disable CA5351 + md5sum = MD5.HashData(password); +#pragma warning restore CA5351 + } + else + { + Array.Copy(md5sum, 0, result, 0, Md5Length); + Array.Copy(password, 0, result, Md5Length, password.Length); +#pragma warning disable CA5351 + md5sum = MD5.HashData(result); +#pragma warning restore CA5351 + } + Array.Copy(md5sum, 0, key, i, Math.Min(Md5Length, keylen - i)); + i += Md5Length; + } + } + + public void DeriveSessionKey(byte[] salt, byte[] masterKey, byte[] sessionKey) + { + HKDF.DeriveKey( + HashAlgorithmName.SHA1, + masterKey.AsSpan(0, KeyLength), + sessionKey.AsSpan(0, KeyLength), + salt.AsSpan(0, SaltLength), + InfoBytes); + } + + protected void IncrementNonce(bool isEncrypt) + { + lock (_nonceIncrementLock) + { + byte[] nonce = isEncrypt ? EncryptNonce : DecryptNonce; + // Shadowsocks AEAD treats the nonce as a little-endian unsigned integer. + for (int i = 0; i < NonceLength; i++) + { + nonce[i]++; + if (nonce[i] != 0) + break; + } + } + } + + public virtual void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) + { + if (isEncrypt) + { + EncryptSalt = new byte[SaltLength]; + Array.Copy(salt, EncryptSalt, SaltLength); + } + else + { + DecryptSalt = new byte[SaltLength]; + Array.Copy(salt, DecryptSalt, SaltLength); + } + logger.Dump("Salt", salt, SaltLength); + } + + public static void randBytes(byte[] buf, int length) { RNG.GetBytes(buf, length); } + + public abstract void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen); + + public abstract void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen); + + #region TCP + + public override void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + Debug.Assert(_encCircularBuffer != null, "_encCircularBuffer != null"); + + _encCircularBuffer.Put(buf, 0, length); + outlength = 0; + logger.Trace("---Start Encryption"); + if (!_encryptSaltSent) + { + _encryptSaltSent = true; + // Generate salt + byte[] saltBytes = new byte[SaltLength]; + randBytes(saltBytes, SaltLength); + InitCipher(saltBytes, true, false); + Array.Copy(saltBytes, 0, outbuf, 0, SaltLength); + outlength = SaltLength; + logger.Trace($"_encryptSaltSent outlength {outlength}"); + } + + if (!_tcpRequestSent) + { + _tcpRequestSent = true; + // The first TCP request + int encAddrBufLength; + byte[] encAddrBufBytes = new byte[AddrBufLength + TagLength * 2 + ChunkLengthBytes]; + byte[] addrBytes = _encCircularBuffer.Read(AddrBufLength); + ChunkEncrypt(addrBytes, AddrBufLength, encAddrBufBytes, out encAddrBufLength); + Debug.Assert(encAddrBufLength == AddrBufLength + TagLength * 2 + ChunkLengthBytes); + Array.Copy(encAddrBufBytes, 0, outbuf, outlength, encAddrBufLength); + outlength += encAddrBufLength; + logger.Trace($"_tcpRequestSent outlength {outlength}"); + } + + // handle other chunks + while (true) + { + uint bufSize = (uint)_encCircularBuffer.Size; + if (bufSize <= 0) return; + var chunklength = (int)Math.Min(bufSize, ChunkLengthMask); + byte[] chunkBytes = _encCircularBuffer.Read(chunklength); + int encChunkLength; + byte[] encChunkBytes = new byte[chunklength + TagLength * 2 + ChunkLengthBytes]; + ChunkEncrypt(chunkBytes, chunklength, encChunkBytes, out encChunkLength); + Debug.Assert(encChunkLength == chunklength + TagLength * 2 + ChunkLengthBytes); + Buffer.BlockCopy(encChunkBytes, 0, outbuf, outlength, encChunkLength); + outlength += encChunkLength; + logger.Trace("chunks enc outlength " + outlength); + // check if we have enough space for outbuf + if (outlength + TransportBufferSizes.ChunkOverheadSize > TransportBufferSizes.BufferSize) + { + logger.Trace("enc outbuf almost full, giving up"); + return; + } + bufSize = (uint)_encCircularBuffer.Size; + if (bufSize <= 0) + { + logger.Trace("No more data to encrypt, leaving"); + return; + } + } + } + + + public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) + { + Debug.Assert(_decCircularBuffer != null, "_decCircularBuffer != null"); + int bufSize; + outlength = 0; + // drop all into buffer + _decCircularBuffer.Put(buf, 0, length); + + logger.Trace("---Start Decryption"); + if (!_decryptSaltReceived) + { + bufSize = _decCircularBuffer.Size; + // check if we get the leading salt + if (bufSize <= SaltLength) + { + // need more + return; + } + _decryptSaltReceived = true; + byte[] salt = _decCircularBuffer.Read(SaltLength); + InitCipher(salt, false, false); + logger.Trace("get salt len " + SaltLength); + } + + // handle chunks + while (true) + { + bufSize = _decCircularBuffer.Size; + // check if we have any data + if (bufSize <= 0) + { + logger.Trace("No data in _decCircularBuffer"); + return; + } + + // first get chunk length + if (bufSize <= ChunkLengthBytes + TagLength) + { + // so we only have chunk length and its tag? + return; + } + + #region Chunk Decryption + + byte[] encLenBytes = _decCircularBuffer.Peek(ChunkLengthBytes + TagLength); + uint decChunkLenLength = 0; + byte[] decChunkLenBytes = new byte[ChunkLengthBytes]; + // try to dec chunk len + cipherDecrypt(encLenBytes, ChunkLengthBytes + (uint)TagLength, decChunkLenBytes, ref decChunkLenLength); + Debug.Assert(decChunkLenLength == ChunkLengthBytes); + // finally we get the real chunk len + ushort chunkLen = (ushort)IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0)); + if (chunkLen > ChunkLengthMask) + { + // we get invalid chunk + logger.Error($"Invalid chunk length: {chunkLen}"); + throw new CryptoErrorException(); + } + logger.Trace("Get the real chunk len:" + chunkLen); + bufSize = _decCircularBuffer.Size; + if (bufSize < ChunkLengthBytes + TagLength /* we haven't remove them */+ chunkLen + TagLength) + { + logger.Trace("No more data to decrypt one chunk"); + return; + } + IncrementNonce(false); + + // we have enough data to decrypt one chunk + // drop chunk len and its tag from buffer + _decCircularBuffer.Skip(ChunkLengthBytes + TagLength); + byte[] encChunkBytes = _decCircularBuffer.Read(chunkLen + TagLength); + byte[] decChunkBytes = new byte[chunkLen]; + uint decChunkLen = 0; + cipherDecrypt(encChunkBytes, chunkLen + (uint)TagLength, decChunkBytes, ref decChunkLen); + Debug.Assert(decChunkLen == chunkLen); + IncrementNonce(false); + + #endregion + + // output to outbuf + Buffer.BlockCopy(decChunkBytes, 0, outbuf, outlength, (int)decChunkLen); + outlength += (int)decChunkLen; + logger.Trace("aead dec outlength " + outlength); + if (outlength + 100 > TransportBufferSizes.BufferSize) + { + logger.Trace("dec outbuf almost full, giving up"); + return; + } + bufSize = _decCircularBuffer.Size; + // check if we already done all of them + if (bufSize <= 0) + { + logger.Trace("No data in _decCircularBuffer, already all done"); + return; + } + } + } + + #endregion + + #region UDP + + public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + // Generate salt + randBytes(outbuf, SaltLength); + InitCipher(outbuf, true, true); + uint olen = 0; + lock (s_udpTempBuffer) + { + cipherEncrypt(buf, (uint)length, s_udpTempBuffer, ref olen); + Debug.Assert(olen == length + TagLength); + Buffer.BlockCopy(s_udpTempBuffer, 0, outbuf, SaltLength, (int)olen); + outlength = (int)(SaltLength + olen); + } + } + + public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) + { + InitCipher(buf, false, true); + uint olen = 0; + lock (s_udpTempBuffer) + { + // copy remaining data to first pos + Buffer.BlockCopy(buf, SaltLength, buf, 0, length - SaltLength); + cipherDecrypt(buf, (uint)(length - SaltLength), s_udpTempBuffer, ref olen); + Buffer.BlockCopy(s_udpTempBuffer, 0, outbuf, 0, (int)olen); + outlength = (int)olen; + } + } + + #endregion + + // we know the plaintext length before encryption, so we can do it in one operation + private void ChunkEncrypt(byte[] plaintext, int plainLen, byte[] ciphertext, out int cipherLen) + { + if (plainLen > ChunkLengthMask) + { + logger.Error("enc chunk too big"); + throw new CryptoErrorException(); + } + + // encrypt len + byte[] encLenBytes = new byte[ChunkLengthBytes + TagLength]; + uint encChunkLenLength = 0; + byte[] lenbuf = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)plainLen)); + cipherEncrypt(lenbuf, ChunkLengthBytes, encLenBytes, ref encChunkLenLength); + Debug.Assert(encChunkLenLength == ChunkLengthBytes + TagLength); + IncrementNonce(true); + + // encrypt corresponding data + byte[] encBytes = new byte[plainLen + TagLength]; + uint encBufLength = 0; + cipherEncrypt(plaintext, (uint)plainLen, encBytes, ref encBufLength); + Debug.Assert(encBufLength == plainLen + TagLength); + IncrementNonce(true); + + // construct outbuf + Array.Copy(encLenBytes, 0, ciphertext, 0, (int)encChunkLenLength); + Buffer.BlockCopy(encBytes, 0, ciphertext, (int)encChunkLenLength, (int)encBufLength); + cipherLen = (int)(encChunkLenLength + encBufLength); + } + } +} diff --git a/shadowsocks-csharp/Encryption/CircularBuffer/ByteCircularBuffer.cs b/Shadowsocks.Core/Encryption/CircularBuffer/ByteCircularBuffer.cs similarity index 91% rename from shadowsocks-csharp/Encryption/CircularBuffer/ByteCircularBuffer.cs rename to Shadowsocks.Core/Encryption/CircularBuffer/ByteCircularBuffer.cs index 61ec2dda2..4e0366767 100644 --- a/shadowsocks-csharp/Encryption/CircularBuffer/ByteCircularBuffer.cs +++ b/Shadowsocks.Core/Encryption/CircularBuffer/ByteCircularBuffer.cs @@ -1,510 +1,506 @@ -#region Original License - -//New BSD License(BSD) -// -//Copyright(c) 2014-2015 Cyotek Ltd -//Copyright(c) 2012, Alex Regueiro -//All rights reserved. -// -//Redistribution and use in source and binary forms, with or without -//modification, are permitted provided that the following conditions are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of Cyotek nor the -// names of its contributors may be used to endorse or promote products -// derived from this software without specific prior written permission. -// -//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -//DISCLAIMED.IN NO EVENT SHALL BE LIABLE FOR ANY -//DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#endregion - -using System; -using System.Collections.Generic; - -namespace Shadowsocks.Encryption.CircularBuffer -{ - /// - /// Represents a first-in, first-out collection of objects using a fixed buffer. - /// - /// - /// The capacity of a is the number of elements the can hold. - /// ByteCircularBuffer accepts null as a valid value for reference types and allows duplicate elements. - /// The methods will remove the items that are returned from the ByteCircularBuffer. To view the contents of the ByteCircularBuffer without removing items, use the or methods. - /// - public class ByteCircularBuffer - { - // based on http://circularbuffer.codeplex.com/ - // http://en.wikipedia.org/wiki/Circular_buffer - // modified from https://github.com/cyotek/Cyotek.Collections.Generic.CircularBuffer - // some code taken from https://github.com/xorxornop/RingBuffer - // and https://github.com/xorxornop/PerfCopy - - #region Instance Fields - - private byte[] _buffer; - - private int _capacity; - - #endregion - - #region Public Constructors - - /// - /// Initializes a new instance of the class that is empty and has the specified initial capacity. - /// - /// The maximum capcity of the buffer. - /// Thown if the is less than zero. - public ByteCircularBuffer(int capacity) - { - if (capacity < 0) - { - throw new ArgumentException("The buffer capacity must be greater than or equal to zero.", - nameof(capacity)); - } - - _buffer = new byte[capacity]; - this.Capacity = capacity; - this.Size = 0; - this.Head = 0; - this.Tail = 0; - } - - #endregion - - #region Public Properties - - /// - /// Gets or sets the total number of elements the internal data structure can hold. - /// - /// The total number of elements that the can contain. - /// Thrown if the specified new capacity is smaller than the current contents of the buffer. - public int Capacity - { - get { return _capacity; } - set - { - if (value != _capacity) - { - if (value < this.Size) - { - throw new ArgumentOutOfRangeException(nameof(value), value, - "The new capacity must be greater than or equal to the buffer size."); - } - - var newBuffer = new byte[value]; - if (this.Size > 0) - { - this.CopyTo(newBuffer); - } - - _buffer = newBuffer; - - _capacity = value; - } - } - } - - /// - /// Gets the index of the beginning of the buffer data. - /// - /// The index of the first element in the buffer. - public int Head { get; protected set; } - - /// - /// Gets a value indicating whether the buffer is empty. - /// - /// true if buffer is empty; otherwise, false. - public virtual bool IsEmpty => this.Size == 0; - - /// - /// Gets a value indicating whether the buffer is full. - /// - /// true if the buffer is full; otherwise, false. - /// The property always returns false if the property is set to true. - public virtual bool IsFull => this.Size == this.Capacity; - - /// - /// Gets the number of elements contained in the . - /// - /// The number of elements contained in the . - public int Size { get; protected set; } - - /// - /// Gets the index of the end of the buffer data. - /// - /// The index of the last element in the buffer. - public int Tail { get; protected set; } - - #endregion - - #region Public Members - - /// - /// Removes all items from the . - /// - public void Clear() - { - this.Size = 0; - this.Head = 0; - this.Tail = 0; - _buffer = new byte[this.Capacity]; - } - - /// - /// Determines whether the contains a specific value. - /// - /// The object to locate in the . - /// true if is found in the ; otherwise, false. - public bool Contains(byte item) - { - var bufferIndex = this.Head; - var comparer = EqualityComparer.Default; - var result = false; - - for (int i = 0; i < this.Size; i++, bufferIndex++) - { - if (bufferIndex == this.Capacity) - { - bufferIndex = 0; - } - - if (comparer.Equals(_buffer[bufferIndex], item)) - { - result = true; - break; - } - } - - return result; - } - - /// - /// Copies the entire to a compatible one-dimensional array, starting at the beginning of the target array. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - public void CopyTo(byte[] array) - { - this.CopyTo(array, 0); - } - - /// - /// Copies the entire to a compatible one-dimensional array, starting at the specified index of the target array. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - public void CopyTo(byte[] array, int arrayIndex) - { - this.CopyTo(this.Head, array, arrayIndex, Math.Min(this.Size, array.Length - arrayIndex)); - } - - /// - /// Copies a range of elements from the to a compatible one-dimensional array, starting at the specified index of the target array. - /// - /// The zero-based index in the source at which copying begins. - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// The number of elements to copy. - public virtual void CopyTo(int index, byte[] array, int arrayIndex, int count) - { - if (count > this.Size) - { - throw new ArgumentOutOfRangeException(nameof(count), count, - "The read count cannot be greater than the buffer size."); - } - - var startAnchor = index; - var dstIndex = arrayIndex; - - while (count > 0) - { - int chunk = Math.Min(Capacity - startAnchor, count); - Buffer.BlockCopy(_buffer, startAnchor, array, dstIndex, chunk); - startAnchor = (startAnchor + chunk == Capacity) ? 0 : startAnchor + chunk; - dstIndex += chunk; - count -= chunk; - } - } - - /// - /// Removes and returns the specified number of objects from the beginning of the . - /// - /// The number of elements to remove and return from the . - /// The objects that are removed from the beginning of the . - public byte[] Get(int count) - { - if (count <= 0) throw new ArgumentOutOfRangeException("should greater than 0"); - var result = new byte[count]; - - this.Get(result); - - return result; - } - - /// - /// Copies and removes the specified number elements from the to a compatible one-dimensional array, starting at the beginning of the target array. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The actual number of elements copied into . - public int Get(byte[] array) - { - if (array.Length <= 0) throw new ArgumentOutOfRangeException("should greater than 0"); - return this.Get(array, 0, array.Length); - } - - /// - /// Copies and removes the specified number elements from the to a compatible one-dimensional array, starting at the specified index of the target array. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// The number of elements to copy. - /// The actual number of elements copied into . - public virtual int Get(byte[] array, int arrayIndex, int count) - { - if (arrayIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Negative offset specified. Offsets must be positive."); - } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "Negative count specified. Count must be positive."); - } - if (count > this.Size) - { - throw new ArgumentException("Ringbuffer contents insufficient for take/read operation.", nameof(count)); - } - if (array.Length < arrayIndex + count) - { - throw new ArgumentException("Destination array too small for requested output."); - } - var bytesCopied = 0; - var dstIndex = arrayIndex; - while (count > 0) - { - int chunk = Math.Min(Capacity - this.Head, count); - Buffer.BlockCopy(_buffer, this.Head, array, dstIndex, chunk); - this.Head = (this.Head + chunk == Capacity) ? 0 : this.Head + chunk; - this.Size -= chunk; - dstIndex += chunk; - bytesCopied += chunk; - count -= chunk; - } - return bytesCopied; - } - - /// - /// Removes and returns the object at the beginning of the . - /// - /// The object that is removed from the beginning of the . - /// Thrown if the buffer is empty. - /// This method is similar to the method, but Peek does not modify the . - public virtual byte Get() - { - if (this.IsEmpty) - { - throw new InvalidOperationException("The buffer is empty."); - } - - var item = _buffer[this.Head]; - if (++this.Head == this.Capacity) - { - this.Head = 0; - } - this.Size--; - - return item; - } - - /// - /// Returns the object at the beginning of the without removing it. - /// - /// The object at the beginning of the . - /// Thrown if the buffer is empty. - public virtual byte Peek() - { - if (this.IsEmpty) - { - throw new InvalidOperationException("The buffer is empty."); - } - - var item = _buffer[this.Head]; - - return item; - } - - /// - /// Returns the specified number of objects from the beginning of the . - /// - /// The number of elements to return from the . - /// The objects that from the beginning of the . - /// Thrown if the buffer is empty. - public virtual byte[] Peek(int count) - { - if (this.IsEmpty) - { - throw new InvalidOperationException("The buffer is empty."); - } - - var items = new byte[count]; - this.CopyTo(items); - - return items; - } - - /// - /// Returns the object at the end of the without removing it. - /// - /// The object at the end of the . - /// Thrown if the buffer is empty. - public virtual byte PeekLast() - { - int bufferIndex; - - if (this.IsEmpty) - { - throw new InvalidOperationException("The buffer is empty."); - } - - if (this.Tail == 0) - { - bufferIndex = this.Size - 1; - } - else - { - bufferIndex = this.Tail - 1; - } - - var item = _buffer[bufferIndex]; - - return item; - } - - /// - /// Copies an entire compatible one-dimensional array to the . - /// - /// The one-dimensional that is the source of the elements copied to . The must have zero-based indexing. - /// Thrown if buffer does not have sufficient capacity to put in new items. - /// If plus the size of exceeds the capacity of the and the property is true, the oldest items in the are overwritten with . - public int Put(byte[] array) - { - return this.Put(array, 0, array.Length); - } - - /// - /// Copies a range of elements from a compatible one-dimensional array to the . - /// - /// The one-dimensional that is the source of the elements copied to . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// The number of elements to copy. - /// Thrown if buffer does not have sufficient capacity to put in new items. - /// If plus exceeds the capacity of the and the property is true, the oldest items in the are overwritten with . - public virtual int Put(byte[] array, int arrayIndex, int count) - { - if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive."); - if (this.Size + count > this.Capacity) - { - throw new InvalidOperationException("The buffer does not have sufficient capacity to put new items."); - } - - if (array.Length < arrayIndex + count) - { - throw new ArgumentException("Source array too small for requested input."); - } - var srcIndex = arrayIndex; - var bytesToProcess = count; - while (bytesToProcess > 0) - { - int chunk = Math.Min(Capacity - Tail, bytesToProcess); - Buffer.BlockCopy(array, srcIndex, _buffer, Tail, chunk); - Tail = (Tail + chunk == Capacity) ? 0 : Tail + chunk; - this.Size += chunk; - srcIndex += chunk; - bytesToProcess -= chunk; - } - - return count; - } - - /// - /// Adds a byte to the end of the . - /// - /// The byte to add to the . - /// Thrown if buffer does not have sufficient capacity to put in new items. - public virtual void Put(byte item) - { - if (IsFull) - { - throw new InvalidOperationException("The buffer does not have sufficient capacity to put new items."); - } - - _buffer[this.Tail] = item; - - this.Tail++; - if (this.Size == this.Capacity) - { - this.Head++; - if (this.Head >= this.Capacity) - { - this.Head -= this.Capacity; - } - } - - if (this.Tail == this.Capacity) - { - this.Tail = 0; - } - - if (this.Size != this.Capacity) - { - this.Size++; - } - } - - /// - /// Increments the starting index of the data buffer in the . - /// - /// The number of elements to increment the data buffer start index by. - public void Skip(int count) - { - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "Negative count specified. Count must be positive."); - } - if (count > this.Size) - { - throw new ArgumentException("Ringbuffer contents insufficient for operation.", nameof(count)); - } - - // Modular division gives new offset position - this.Head = (this.Head + count) % Capacity; - this.Size -= count; - } - - /// - /// Copies the elements to a new array. - /// - /// A new array containing elements copied from the . - /// The is not modified. The order of the elements in the new array is the same as the order of the elements from the beginning of the to its end. - public byte[] ToArray() - { - var result = new byte[this.Size]; - - this.CopyTo(result); - - return result; - } - - #endregion - } -} \ No newline at end of file +#region Original License + +//New BSD License(BSD) +// +//Copyright(c) 2014-2015 Cyotek Ltd +//Copyright(c) 2012, Alex Regueiro +//All rights reserved. +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Cyotek nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED.IN NO EVENT SHALL BE LIABLE FOR ANY +//DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Collections.Generic; + +namespace Shadowsocks.Encryption.CircularBuffer +{ + /// + /// Represents a first-in, first-out collection of objects using a fixed buffer. + /// + /// + /// The capacity of a is the number of elements the can hold. + /// ByteCircularBuffer accepts null as a valid value for reference types and allows duplicate elements. + /// The methods will remove the items that are returned from the ByteCircularBuffer. To view the contents of the ByteCircularBuffer without removing items, use the or methods. + /// + public class ByteCircularBuffer + { + // based on http://circularbuffer.codeplex.com/ + // http://en.wikipedia.org/wiki/Circular_buffer + // modified from https://github.com/cyotek/Cyotek.Collections.Generic.CircularBuffer + // some code taken from https://github.com/xorxornop/RingBuffer + // and https://github.com/xorxornop/PerfCopy + + #region Instance Fields + + private byte[] _buffer; + + private int _capacity; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + /// The maximum capcity of the buffer. + /// Thown if the is less than zero. + public ByteCircularBuffer(int capacity) + { + if (capacity < 0) + { + throw new ArgumentException("The buffer capacity must be greater than or equal to zero.", + nameof(capacity)); + } + + _buffer = new byte[capacity]; + this.Capacity = capacity; + this.Size = 0; + this.Head = 0; + this.Tail = 0; + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the total number of elements the internal data structure can hold. + /// + /// The total number of elements that the can contain. + /// Thrown if the specified new capacity is smaller than the current contents of the buffer. + public int Capacity + { + get { return _capacity; } + set + { + if (value != _capacity) + { + if (value < this.Size) + { + throw new ArgumentOutOfRangeException(nameof(value), value, + "The new capacity must be greater than or equal to the buffer size."); + } + + var newBuffer = new byte[value]; + if (this.Size > 0) + { + this.CopyTo(newBuffer); + } + + _buffer = newBuffer; + + _capacity = value; + } + } + } + + /// + /// Gets the index of the beginning of the buffer data. + /// + /// The index of the first element in the buffer. + public int Head { get; protected set; } + + /// + /// Gets a value indicating whether the buffer is empty. + /// + /// true if buffer is empty; otherwise, false. + public virtual bool IsEmpty => this.Size == 0; + + /// + /// Gets a value indicating whether the buffer is full. + /// + /// true if the buffer is full; otherwise, false. + /// The property always returns false if the property is set to true. + public virtual bool IsFull => this.Size == this.Capacity; + + /// + /// Gets the number of elements contained in the . + /// + /// The number of elements contained in the . + public int Size { get; protected set; } + + /// + /// Gets the index of the end of the buffer data. + /// + /// The index of the last element in the buffer. + public int Tail { get; protected set; } + + #endregion + + #region Public Members + + /// + /// Removes all items from the . + /// + public void Clear() + { + this.Size = 0; + this.Head = 0; + this.Tail = 0; + _buffer = new byte[this.Capacity]; + } + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// true if is found in the ; otherwise, false. + public bool Contains(byte item) + { + var bufferIndex = this.Head; + var comparer = EqualityComparer.Default; + var result = false; + + for (int i = 0; i < this.Size; i++, bufferIndex++) + { + if (bufferIndex == this.Capacity) + { + bufferIndex = 0; + } + + if (comparer.Equals(_buffer[bufferIndex], item)) + { + result = true; + break; + } + } + + return result; + } + + /// + /// Copies the entire to a compatible one-dimensional array, starting at the beginning of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + public void CopyTo(byte[] array) + { + this.CopyTo(array, 0); + } + + /// + /// Copies the entire to a compatible one-dimensional array, starting at the specified index of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + public void CopyTo(byte[] array, int arrayIndex) + { + this.CopyTo(this.Head, array, arrayIndex, Math.Min(this.Size, array.Length - arrayIndex)); + } + + /// + /// Copies a range of elements from the to a compatible one-dimensional array, starting at the specified index of the target array. + /// + /// The zero-based index in the source at which copying begins. + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// The number of elements to copy. + public virtual void CopyTo(int index, byte[] array, int arrayIndex, int count) + { + if (count > this.Size) + { + throw new ArgumentOutOfRangeException(nameof(count), count, + "The read count cannot be greater than the buffer size."); + } + + var startAnchor = index; + var dstIndex = arrayIndex; + + while (count > 0) + { + int chunk = Math.Min(Capacity - startAnchor, count); + Buffer.BlockCopy(_buffer, startAnchor, array, dstIndex, chunk); + startAnchor = (startAnchor + chunk == Capacity) ? 0 : startAnchor + chunk; + dstIndex += chunk; + count -= chunk; + } + } + + /// + /// Removes and returns the specified number of objects from the beginning of the . + /// + /// The number of elements to remove and return from the . + /// The objects that are removed from the beginning of the . + public byte[] Read(int count) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); + var result = new byte[count]; + + Read(result); + + return result; + } + + /// + /// Copies and removes the specified number elements from the to a compatible one-dimensional array, starting at the beginning of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The actual number of elements copied into . + public int Read(byte[] array) + { + ArgumentNullException.ThrowIfNull(array); + if (array.Length == 0) throw new ArgumentException("Array must not be empty.", nameof(array)); + return Read(array, 0, array.Length); + } + + /// + /// Copies and removes the specified number elements from the to a compatible one-dimensional array, starting at the specified index of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// The number of elements to copy. + /// The actual number of elements copied into . + public virtual int Read(byte[] array, int arrayIndex, int count) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + ArgumentOutOfRangeException.ThrowIfNegative(count); + if (count > this.Size) + { + throw new ArgumentException("Ringbuffer contents insufficient for take/read operation.", nameof(count)); + } + if (array.Length < arrayIndex + count) + { + throw new ArgumentException("Destination array too small for requested output."); + } + var bytesCopied = 0; + var dstIndex = arrayIndex; + while (count > 0) + { + int chunk = Math.Min(Capacity - this.Head, count); + Buffer.BlockCopy(_buffer, this.Head, array, dstIndex, chunk); + this.Head = (this.Head + chunk == Capacity) ? 0 : this.Head + chunk; + this.Size -= chunk; + dstIndex += chunk; + bytesCopied += chunk; + count -= chunk; + } + return bytesCopied; + } + + /// + /// Removes and returns the object at the beginning of the . + /// + /// The object that is removed from the beginning of the . + /// Thrown if the buffer is empty. + /// This method is similar to the method, but Peek does not modify the . + public virtual byte Read() + { + if (this.IsEmpty) + { + throw new InvalidOperationException("The buffer is empty."); + } + + var item = _buffer[this.Head]; + if (++this.Head == this.Capacity) + { + this.Head = 0; + } + this.Size--; + + return item; + } + + /// + /// Returns the object at the beginning of the without removing it. + /// + /// The object at the beginning of the . + /// Thrown if the buffer is empty. + public virtual byte Peek() + { + if (this.IsEmpty) + { + throw new InvalidOperationException("The buffer is empty."); + } + + var item = _buffer[this.Head]; + + return item; + } + + /// + /// Returns the specified number of objects from the beginning of the . + /// + /// The number of elements to return from the . + /// The objects that from the beginning of the . + /// Thrown if the buffer is empty. + public virtual byte[] Peek(int count) + { + if (this.IsEmpty) + { + throw new InvalidOperationException("The buffer is empty."); + } + + var items = new byte[count]; + this.CopyTo(items); + + return items; + } + + /// + /// Returns the object at the end of the without removing it. + /// + /// The object at the end of the . + /// Thrown if the buffer is empty. + public virtual byte PeekLast() + { + int bufferIndex; + + if (this.IsEmpty) + { + throw new InvalidOperationException("The buffer is empty."); + } + + if (this.Tail == 0) + { + bufferIndex = this.Size - 1; + } + else + { + bufferIndex = this.Tail - 1; + } + + var item = _buffer[bufferIndex]; + + return item; + } + + /// + /// Copies an entire compatible one-dimensional array to the . + /// + /// The one-dimensional that is the source of the elements copied to . The must have zero-based indexing. + /// Thrown if buffer does not have sufficient capacity to put in new items. + /// If plus the size of exceeds the capacity of the and the property is true, the oldest items in the are overwritten with . + public int Put(byte[] array) + { + ArgumentNullException.ThrowIfNull(array); + return Put(array, 0, array.Length); + } + + /// + /// Copies a range of elements from a compatible one-dimensional array to the . + /// + /// The one-dimensional that is the source of the elements copied to . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// The number of elements to copy. + /// Thrown if buffer does not have sufficient capacity to put in new items. + /// If plus exceeds the capacity of the and the property is true, the oldest items in the are overwritten with . + public virtual int Put(byte[] array, int arrayIndex, int count) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); + if (this.Size + count > this.Capacity) + { + throw new InvalidOperationException("The buffer does not have sufficient capacity to put new items."); + } + + if (array.Length < arrayIndex + count) + { + throw new ArgumentException("Source array too small for requested input."); + } + var srcIndex = arrayIndex; + var bytesToProcess = count; + while (bytesToProcess > 0) + { + int chunk = Math.Min(Capacity - Tail, bytesToProcess); + Buffer.BlockCopy(array, srcIndex, _buffer, Tail, chunk); + Tail = (Tail + chunk == Capacity) ? 0 : Tail + chunk; + this.Size += chunk; + srcIndex += chunk; + bytesToProcess -= chunk; + } + + return count; + } + + /// + /// Adds a byte to the end of the . + /// + /// The byte to add to the . + /// Thrown if buffer does not have sufficient capacity to put in new items. + public virtual void Put(byte item) + { + if (IsFull) + { + throw new InvalidOperationException("The buffer does not have sufficient capacity to put new items."); + } + + _buffer[this.Tail] = item; + + this.Tail++; + if (this.Size == this.Capacity) + { + this.Head++; + if (this.Head >= this.Capacity) + { + this.Head -= this.Capacity; + } + } + + if (this.Tail == this.Capacity) + { + this.Tail = 0; + } + + if (this.Size != this.Capacity) + { + this.Size++; + } + } + + /// + /// Increments the starting index of the data buffer in the . + /// + /// The number of elements to increment the data buffer start index by. + public void Skip(int count) + { + ArgumentOutOfRangeException.ThrowIfNegative(count); + if (count > this.Size) + { + throw new ArgumentException("Ringbuffer contents insufficient for operation.", nameof(count)); + } + + // Modular division gives new offset position + this.Head = (this.Head + count) % Capacity; + this.Size -= count; + } + + /// + /// Copies the elements to a new array. + /// + /// A new array containing elements copied from the . + /// The is not modified. The order of the elements in the new array is the same as the order of the elements from the beginning of the to its end. + public byte[] ToArray() + { + var result = new byte[this.Size]; + + this.CopyTo(result); + + return result; + } + + #endregion + } +} diff --git a/shadowsocks-csharp/Encryption/EncryptorBase.cs b/Shadowsocks.Core/Encryption/EncryptorBase.cs similarity index 72% rename from shadowsocks-csharp/Encryption/EncryptorBase.cs rename to Shadowsocks.Core/Encryption/EncryptorBase.cs index ab69f1eca..737fc230e 100644 --- a/shadowsocks-csharp/Encryption/EncryptorBase.cs +++ b/Shadowsocks.Core/Encryption/EncryptorBase.cs @@ -1,97 +1,97 @@ -namespace Shadowsocks.Encryption -{ - public class EncryptorInfo - { - public int KeySize; - public int IvSize; - public int SaltSize; - public int TagSize; - public int NonceSize; - public int Type; - public string InnerLibName; - - // For those who make use of internal crypto method name - // e.g. mbed TLS - - #region Stream ciphers - - public EncryptorInfo(string innerLibName, int keySize, int ivSize, int type) - { - this.KeySize = keySize; - this.IvSize = ivSize; - this.Type = type; - this.InnerLibName = innerLibName; - } - - public EncryptorInfo(int keySize, int ivSize, int type) - { - this.KeySize = keySize; - this.IvSize = ivSize; - this.Type = type; - this.InnerLibName = string.Empty; - } - - #endregion - - #region AEAD ciphers - - public EncryptorInfo(string innerLibName, int keySize, int saltSize, int nonceSize, int tagSize, int type) - { - this.KeySize = keySize; - this.SaltSize = saltSize; - this.NonceSize = nonceSize; - this.TagSize = tagSize; - this.Type = type; - this.InnerLibName = innerLibName; - } - - public EncryptorInfo(int keySize, int saltSize, int nonceSize, int tagSize, int type) - { - this.KeySize = keySize; - this.SaltSize = saltSize; - this.NonceSize = nonceSize; - this.TagSize = tagSize; - this.Type = type; - this.InnerLibName = string.Empty; - } - - #endregion - } - - public abstract class EncryptorBase - : IEncryptor - { - public const int MAX_INPUT_SIZE = 32768; - - public const int MAX_DOMAIN_LEN = 255; - public const int ADDR_PORT_LEN = 2; - public const int ADDR_ATYP_LEN = 1; - - public const int ATYP_IPv4 = 0x01; - public const int ATYP_DOMAIN = 0x03; - public const int ATYP_IPv6 = 0x04; - - public const int MD5_LEN = 16; - - protected EncryptorBase(string method, string password) - { - Method = method; - Password = password; - } - - protected string Method; - protected string Password; - - public abstract void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength); - - public abstract void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength); - - public abstract void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); - - public abstract void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); - - public abstract void Dispose(); - - public int AddrBufLength { get; set; } = - 1; - } -} \ No newline at end of file +namespace Shadowsocks.Encryption +{ + public class EncryptorInfo + { + public int KeySize { get; set; } + public int IvSize { get; set; } + public int SaltSize { get; set; } + public int TagSize { get; set; } + public int NonceSize { get; set; } + public int Type { get; set; } + public string InnerLibName { get; set; } + + // For those who make use of internal crypto method name + // e.g. mbed TLS + + #region Stream ciphers + + public EncryptorInfo(string innerLibName, int keySize, int ivSize, int type) + { + this.KeySize = keySize; + this.IvSize = ivSize; + this.Type = type; + this.InnerLibName = innerLibName; + } + + public EncryptorInfo(int keySize, int ivSize, int type) + { + this.KeySize = keySize; + this.IvSize = ivSize; + this.Type = type; + this.InnerLibName = string.Empty; + } + + #endregion + + #region AEAD ciphers + + public EncryptorInfo(string innerLibName, int keySize, int saltSize, int nonceSize, int tagSize, int type) + { + this.KeySize = keySize; + this.SaltSize = saltSize; + this.NonceSize = nonceSize; + this.TagSize = tagSize; + this.Type = type; + this.InnerLibName = innerLibName; + } + + public EncryptorInfo(int keySize, int saltSize, int nonceSize, int tagSize, int type) + { + this.KeySize = keySize; + this.SaltSize = saltSize; + this.NonceSize = nonceSize; + this.TagSize = tagSize; + this.Type = type; + this.InnerLibName = string.Empty; + } + + #endregion + } + + public abstract class EncryptorBase + : IEncryptor + { + public const int MaxInputSize = 32768; + + public const int MaxDomainLength = 255; + public const int AddressPortLength = 2; + public const int AddressTypeLength = 1; + + public const int AddressTypeIPv4 = 0x01; + public const int AddressTypeDomain = 0x03; + public const int AddressTypeIPv6 = 0x04; + + public const int Md5Length = 16; + + protected EncryptorBase(string method, string password) + { + Method = method; + Password = password; + } + + protected string Method { get; } + protected string Password { get; } + + public abstract void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength); + + public abstract void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength); + + public abstract void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + + public abstract void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + + public abstract void Dispose(); + + public int AddrBufLength { get; set; } = -1; + } +} diff --git a/Shadowsocks.Core/Encryption/EncryptorFactory.cs b/Shadowsocks.Core/Encryption/EncryptorFactory.cs new file mode 100644 index 000000000..aa8a2ce20 --- /dev/null +++ b/Shadowsocks.Core/Encryption/EncryptorFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using Shadowsocks.Encryption.AEAD; +using Shadowsocks.Encryption.Stream; + +namespace Shadowsocks.Encryption +{ + public static class EncryptorFactory + { + private static readonly Dictionary _registeredEncryptors = new(); + + private static readonly Type[] ConstructorTypes = { typeof(string), typeof(string) }; + + static EncryptorFactory() + { + foreach (string method in AEADBclEncryptor.SupportedCiphers()) + { + _registeredEncryptors.Add(method, typeof(AEADBclEncryptor)); + } + + foreach (string method in PlainEncryptor.SupportedCiphers()) + { + _registeredEncryptors.Add(method, typeof(PlainEncryptor)); + } + } + + public static IEncryptor GetEncryptor(string method, string password) + { + if (string.IsNullOrEmpty(method)) + { + method = Model.Server.DefaultMethod; + } + + method = method.ToLowerInvariant(); + if (!_registeredEncryptors.TryGetValue(method, out Type t)) + { + throw new NotSupportedException($"Encryption method '{method}' is not supported."); + } + + ConstructorInfo c = t.GetConstructor(ConstructorTypes); + if (c is null) throw new InvalidOperationException($"Encryptor type '{t.FullName}' does not expose the expected constructor."); + IEncryptor result = (IEncryptor)c.Invoke(new object[] { method, password }); + return result; + } + + public static string DumpRegisteredEncryptor() + { + var sb = new StringBuilder(); + sb.Append(Environment.NewLine); + sb.AppendLine("========================="); + sb.AppendLine("Registered Encryptor Info"); + foreach (var encryptor in _registeredEncryptors) + { + sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $"{encryptor.Key}=>{encryptor.Value.Name}"); + } + + sb.AppendLine("========================="); + return sb.ToString(); + } + } +} diff --git a/shadowsocks-csharp/Encryption/Exception/CryptoException.cs b/Shadowsocks.Core/Encryption/Exception/CryptoException.cs similarity index 95% rename from shadowsocks-csharp/Encryption/Exception/CryptoException.cs rename to Shadowsocks.Core/Encryption/Exception/CryptoException.cs index 2871affc3..0ddd98525 100644 --- a/shadowsocks-csharp/Encryption/Exception/CryptoException.cs +++ b/Shadowsocks.Core/Encryption/Exception/CryptoException.cs @@ -1,17 +1,17 @@ -namespace Shadowsocks.Encryption.Exception -{ - public class CryptoErrorException : System.Exception - { - public CryptoErrorException() - { - } - - public CryptoErrorException(string msg) : base(msg) - { - } - - public CryptoErrorException(string message, System.Exception innerException) : base(message, innerException) - { - } - } -} \ No newline at end of file +namespace Shadowsocks.Encryption.Exception +{ + public class CryptoErrorException : System.Exception + { + public CryptoErrorException() + { + } + + public CryptoErrorException(string msg) : base(msg) + { + } + + public CryptoErrorException(string message, System.Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/shadowsocks-csharp/Encryption/IEncryptor.cs b/Shadowsocks.Core/Encryption/IEncryptor.cs similarity index 97% rename from shadowsocks-csharp/Encryption/IEncryptor.cs rename to Shadowsocks.Core/Encryption/IEncryptor.cs index d238083c2..8b636f89a 100644 --- a/shadowsocks-csharp/Encryption/IEncryptor.cs +++ b/Shadowsocks.Core/Encryption/IEncryptor.cs @@ -1,14 +1,14 @@ -using System; - -namespace Shadowsocks.Encryption -{ - public interface IEncryptor : IDisposable - { - /* length == -1 means not used */ - int AddrBufLength { set; get; } - void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength); - void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength); - void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); - void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); - } -} +using System; + +namespace Shadowsocks.Encryption +{ + public interface IEncryptor : IDisposable + { + /* length == -1 means not used */ + int AddrBufLength { set; get; } + void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength); + void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength); + void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength); + } +} diff --git a/shadowsocks-csharp/Encryption/RNG.cs b/Shadowsocks.Core/Encryption/RNG.cs similarity index 56% rename from shadowsocks-csharp/Encryption/RNG.cs rename to Shadowsocks.Core/Encryption/RNG.cs index cd6c49f32..16ce8b375 100644 --- a/shadowsocks-csharp/Encryption/RNG.cs +++ b/Shadowsocks.Core/Encryption/RNG.cs @@ -1,48 +1,41 @@ -using System; -using System.Security.Cryptography; - -namespace Shadowsocks.Encryption -{ - public static class RNG - { - private static RNGCryptoServiceProvider _rng = null; - - public static void Init() - { - _rng = _rng ?? new RNGCryptoServiceProvider(); - } - - public static void Close() - { - _rng?.Dispose(); - _rng = null; - } - - public static void Reload() - { - Close(); - Init(); - } - - public static void GetBytes(byte[] buf) - { - GetBytes(buf, buf.Length); - } - - public static void GetBytes(byte[] buf, int len) - { - if (_rng == null) Init(); - try - { - _rng.GetBytes(buf, 0, len); - } - catch - { - // the backup way - byte[] tmp = new byte[len]; - _rng.GetBytes(tmp); - Buffer.BlockCopy(tmp, 0, buf, 0, len); - } - } - } -} \ No newline at end of file +using System; +using System.Security.Cryptography; + +namespace Shadowsocks.Encryption +{ + public static class RNG + { + private static RandomNumberGenerator _rng; + + public static void Init() + { + _rng = _rng ?? RandomNumberGenerator.Create(); + } + + public static void Close() + { + _rng?.Dispose(); + _rng = null; + } + + public static void Reload() + { + Close(); + Init(); + } + + public static void GetBytes(byte[] buf) + { + ArgumentNullException.ThrowIfNull(buf); + GetBytes(buf, buf.Length); + } + + public static void GetBytes(byte[] buf, int len) + { + if (_rng == null) Init(); + ArgumentNullException.ThrowIfNull(buf); + if ((uint)len > (uint)buf.Length) throw new ArgumentOutOfRangeException(nameof(len)); + _rng.GetBytes(buf.AsSpan(0, len)); + } + } +} diff --git a/shadowsocks-csharp/Encryption/Stream/PlainEncryptor.cs b/Shadowsocks.Core/Encryption/Stream/PlainEncryptor.cs similarity index 90% rename from shadowsocks-csharp/Encryption/Stream/PlainEncryptor.cs rename to Shadowsocks.Core/Encryption/Stream/PlainEncryptor.cs index 8097f4b95..59e1af611 100644 --- a/shadowsocks-csharp/Encryption/Stream/PlainEncryptor.cs +++ b/Shadowsocks.Core/Encryption/Stream/PlainEncryptor.cs @@ -8,7 +8,7 @@ class PlainEncryptor { const int CIPHER_NONE = 1; - private static Dictionary _ciphers = new Dictionary { + private static readonly Dictionary _ciphers = new Dictionary { { "plain", new EncryptorInfo("PLAIN", 0, 0, CIPHER_NONE) }, { "none", new EncryptorInfo("PLAIN", 0, 0, CIPHER_NONE) } }; @@ -22,10 +22,6 @@ public static List SupportedCiphers() return new List(_ciphers.Keys); } - protected Dictionary getCiphers() - { - return _ciphers; - } #region TCP diff --git a/Shadowsocks.Core/FodyWeavers.xml b/Shadowsocks.Core/FodyWeavers.xml new file mode 100644 index 000000000..4872b6b2c --- /dev/null +++ b/Shadowsocks.Core/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Shadowsocks.Core/HttpServerUtilityUrlToken.cs b/Shadowsocks.Core/HttpServerUtilityUrlToken.cs new file mode 100644 index 000000000..badd5d5c7 --- /dev/null +++ b/Shadowsocks.Core/HttpServerUtilityUrlToken.cs @@ -0,0 +1,49 @@ +using System; + +namespace Shadowsocks +{ + /// + /// Compatibility implementation of HttpServerUtility.UrlTokenEncode/UrlTokenDecode. + /// The format is base64url without '=' padding plus one trailing digit containing + /// the removed padding count; this is intentionally not ordinary Base64Url. + /// + public static class HttpServerUtilityUrlToken + { + public static string Encode(byte[] bytes) + { + ArgumentNullException.ThrowIfNull(bytes); + if (bytes.Length == 0) return string.Empty; + + string base64 = Convert.ToBase64String(bytes); + int end = base64.Length; + while (end > 0 && base64[end - 1] == '=') end--; + + char[] result = new char[end + 1]; + for (int i = 0; i < end; i++) + { + char c = base64[i]; + result[i] = c == '+' ? '-' : c == '/' ? '_' : c; + } + result[end] = (char)('0' + (base64.Length - end)); + return new string(result); + } + + public static byte[] Decode(string input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length == 0) return Array.Empty(); + + int padding = input[input.Length - 1] - '0'; + if (padding < 0 || padding > 2) throw new FormatException("Invalid URL token padding."); + + char[] chars = new char[input.Length - 1 + padding]; + for (int i = 0; i < input.Length - 1; i++) + { + char c = input[i]; + chars[i] = c == '-' ? '+' : c == '_' ? '/' : c; + } + for (int i = input.Length - 1; i < chars.Length; i++) chars[i] = '='; + return Convert.FromBase64CharArray(chars, 0, chars.Length); + } + } +} diff --git a/Shadowsocks.Core/IUserInteractionService.cs b/Shadowsocks.Core/IUserInteractionService.cs new file mode 100644 index 000000000..1437efe69 --- /dev/null +++ b/Shadowsocks.Core/IUserInteractionService.cs @@ -0,0 +1,26 @@ +namespace Shadowsocks.Core +{ + /// + /// Presentation/platform boundary used by the core for user-facing operations. + /// Implementations live outside Core; Core itself has no desktop UI reference. + /// + public interface IUserInteractionService + { + bool Confirm(string message, string title); + void ShowInfo(string message, string title = null); + void ShowWarning(string message, string title = null); + void ShowError(string message, string title = null); + void SetClipboardText(string text); + } + + internal sealed class NullUserInteractionService : IUserInteractionService + { + public static readonly NullUserInteractionService Instance = new(); + private NullUserInteractionService() { } + public bool Confirm(string message, string title) => false; + public void ShowInfo(string message, string title = null) { } + public void ShowWarning(string message, string title = null) { } + public void ShowError(string message, string title = null) { } + public void SetClipboardText(string text) { } + } +} diff --git a/Shadowsocks.Core/LegalDocumentLocalizer.cs b/Shadowsocks.Core/LegalDocumentLocalizer.cs new file mode 100644 index 000000000..5334dd35c --- /dev/null +++ b/Shadowsocks.Core/LegalDocumentLocalizer.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System; +using System.Globalization; + +namespace Shadowsocks.Core; + +public static class LegalDocumentLocalizer +{ + public static string CreateProductLicense(string originalLicense, CultureInfo culture) + { + ArgumentException.ThrowIfNullOrWhiteSpace(originalLicense); + ArgumentNullException.ThrowIfNull(culture); + + LegalIntro intro = GetIntro(culture); + string originalTerms = SliceFrom(originalLicense, "GNU GENERAL PUBLIC LICENSE"); + return string.Join(Environment.NewLine, + intro.ProductTitle, + string.Empty, + intro.ProductSummary, + string.Empty, + "SPDX-License-Identifier: GPL-3.0-or-later", + "Project source: https://github.com/SlimRG/shadowsocks-reborn", + string.Empty, + intro.OriginalTermsNotice, + string.Empty, + originalTerms); + } + + public static string CreateThirdPartyNotices(string originalNotices, CultureInfo culture) + { + ArgumentException.ThrowIfNullOrWhiteSpace(originalNotices); + ArgumentNullException.ThrowIfNull(culture); + + LegalIntro intro = GetIntro(culture); + string details = SliceFrom(originalNotices, "## Runtime components included in the application"); + return string.Join(Environment.NewLine, + intro.NoticesTitle, + string.Empty, + intro.NoticesSummary, + string.Empty, + intro.ComponentLicenseNotice, + string.Empty, + intro.OriginalNoticesHeading, + string.Empty, + details); + } + + private static string SliceFrom(string text, string marker) + { + int index = text.IndexOf(marker, StringComparison.Ordinal); + if (index < 0) + { + return text.TrimStart('\uFEFF', '\r', '\n', ' '); + } + + return text[index..].TrimStart(); + } + + private static LegalIntro GetIntro(CultureInfo culture) + { + string name = culture.Name; + string language = culture.TwoLetterISOLanguageName.ToLowerInvariant(); + return language switch + { + "ru" => new( + "Лицензия Shadowsocks Reborn", + "Shadowsocks Reborn распространяется по лицензии GNU General Public License версии 3 или любой более поздней версии (GPL-3.0-or-later). Вы можете использовать, изучать, изменять и распространять программу при соблюдении условий GPL.", + "Ниже приведён оригинальный юридически значимый текст GPL v3 на английском языке. Этот локализованный раздел предназначен для удобства и не заменяет оригинал лицензии.", + "Уведомления сторонних компонентов", + "Ниже перечислены сторонние библиотеки, исходные фрагменты и загружаемые компоненты, используемые Shadowsocks Reborn 5.2.31. Каждый компонент сохраняет собственную лицензию и не перелицензируется как часть Shadowsocks Reborn.", + "Названия лицензий, copyright-уведомления, ограничения ответственности и иные юридические условия сохраняются на языке оригинала, чтобы не искажать их смысл неофициальным переводом.", + "Оригинальные уведомления и тексты лицензий"), + "zh" when name.Contains("TW", StringComparison.OrdinalIgnoreCase) || name.Contains("HK", StringComparison.OrdinalIgnoreCase) || name.Contains("Hant", StringComparison.OrdinalIgnoreCase) => new( + "Shadowsocks Reborn 授權條款", + "Shadowsocks Reborn 依 GNU General Public License 第 3 版或其任何後續版本(GPL-3.0-or-later)散佈。您可在遵守 GPL 條款的前提下使用、研究、修改與重新散佈本程式。", + "下方保留具法律效力的 GPL v3 英文原文。本地化說明僅供閱讀便利,不取代原始授權條款。", + "第三方元件聲明", + "下方列出 Shadowsocks Reborn 5.2.31 使用的第三方程式庫、來源衍生程式碼與按需下載元件。每個元件均保留其自身授權,不因整合至 Shadowsocks Reborn 而變更授權。", + "授權名稱、著作權聲明、免責條款及其他法律文字保留原文,以避免非官方翻譯改變其法律含義。", + "原始第三方聲明與授權文字"), + "zh" => new( + "Shadowsocks Reborn 许可证", + "Shadowsocks Reborn 按 GNU General Public License 第 3 版或任何后续版本(GPL-3.0-or-later)发布。您可以在遵守 GPL 条款的前提下使用、研究、修改和重新分发本程序。", + "下方保留具有法律意义的 GPL v3 英文原文。本地化说明仅为阅读方便,不替代原始许可证条款。", + "第三方组件声明", + "下方列出 Shadowsocks Reborn 5.2.31 使用的第三方库、源代码衍生部分和按需下载组件。每个组件均保留其自身许可证,不会因集成到 Shadowsocks Reborn 而被重新许可。", + "许可证名称、版权声明、免责声明和其他法律文本保留原文,以避免非官方翻译改变其法律含义。", + "原始第三方声明和许可证文本"), + "ja" => new( + "Shadowsocks Reborn ライセンス", + "Shadowsocks Reborn は GNU General Public License バージョン 3 またはそれ以降(GPL-3.0-or-later)の条件で配布されます。GPL の条件に従う限り、利用、調査、変更、再配布ができます。", + "以下には法的に基準となる GPL v3 の英語原文を収録しています。この翻訳説明は読みやすさのためのもので、原文のライセンス条項を置き換えるものではありません。", + "サードパーティコンポーネント通知", + "以下は Shadowsocks Reborn 5.2.31 が使用するサードパーティライブラリ、派生ソースコード、オンデマンドで取得するコンポーネントを示します。各コンポーネントはそれぞれのライセンスに従います。", + "ライセンス名、著作権表示、免責事項などの法的文面は、非公式翻訳による意味の変更を避けるため原文のまま掲載します。", + "サードパーティ通知とライセンスの原文"), + "ko" => new( + "Shadowsocks Reborn 라이선스", + "Shadowsocks Reborn은 GNU General Public License 버전 3 또는 그 이후 버전(GPL-3.0-or-later)에 따라 배포됩니다. GPL 조건을 준수하는 범위에서 프로그램을 사용, 연구, 수정 및 재배포할 수 있습니다.", + "아래에는 법적 기준이 되는 GPL v3 영문 원문을 그대로 제공합니다. 이 현지화 설명은 편의를 위한 것이며 원본 라이선스 조건을 대체하지 않습니다.", + "서드파티 구성 요소 고지", + "아래에는 Shadowsocks Reborn 5.2.31에서 사용하는 서드파티 라이브러리, 파생 소스 코드 및 필요 시 다운로드되는 구성 요소가 나열됩니다. 각 구성 요소는 자체 라이선스를 유지합니다.", + "라이선스 이름, 저작권 고지, 면책 조항 및 기타 법적 문구는 비공식 번역으로 의미가 달라지는 것을 방지하기 위해 원문을 유지합니다.", + "원본 서드파티 고지 및 라이선스 문구"), + "fr" => new( + "Licence de Shadowsocks Reborn", + "Shadowsocks Reborn est distribué selon les termes de la GNU General Public License version 3 ou toute version ultérieure (GPL-3.0-or-later). Vous pouvez utiliser, étudier, modifier et redistribuer le programme dans le respect de la GPL.", + "Le texte anglais original de la GPL v3, qui fait foi juridiquement, est reproduit ci-dessous. Cette présentation localisée est fournie pour faciliter la lecture et ne remplace pas les termes originaux de la licence.", + "Mentions relatives aux composants tiers", + "La section ci-dessous recense les bibliothèques tierces, le code source dérivé et les composants téléchargés à la demande utilisés par Shadowsocks Reborn 5.2.31. Chaque composant conserve sa propre licence.", + "Les noms de licences, mentions de copyright, exclusions de garantie et autres textes juridiques restent dans leur langue d'origine afin qu'une traduction non officielle n'en modifie pas le sens.", + "Mentions tierces et textes de licence originaux"), + _ => new( + "Shadowsocks Reborn License", + "Shadowsocks Reborn is distributed under the GNU General Public License version 3 or any later version (GPL-3.0-or-later). You may use, study, modify and redistribute the program subject to the GPL terms.", + "The legally authoritative GPL v3 text follows in its original English form.", + "Third-party component notices", + "The section below records third-party libraries, source-derived code and on-demand components used by Shadowsocks Reborn 5.2.31. Each component remains subject to its own license.", + "License names, copyright notices, warranty disclaimers and other legal terms are preserved in their original wording.", + "Original third-party notices and license text"), + }; + } + + private sealed record LegalIntro( + string ProductTitle, + string ProductSummary, + string OriginalTermsNotice, + string NoticesTitle, + string NoticesSummary, + string ComponentLicenseNotice, + string OriginalNoticesHeading); +} diff --git a/Shadowsocks.Core/Localization/CsvLocalizationService.cs b/Shadowsocks.Core/Localization/CsvLocalizationService.cs new file mode 100644 index 000000000..871d80677 --- /dev/null +++ b/Shadowsocks.Core/Localization/CsvLocalizationService.cs @@ -0,0 +1,112 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Microsoft.VisualBasic.FileIO; +using NLog; + +namespace Shadowsocks.Localization; + +public sealed class CsvLocalizationService : ILocalizationService +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly Dictionary _strings = new(StringComparer.Ordinal); + + public CsvLocalizationService(string csv, CultureInfo? culture = null) + { + ArgumentNullException.ThrowIfNull(csv); + Culture = culture ?? CultureInfo.CurrentCulture; + Load(csv, Culture.Name); + } + + public CultureInfo Culture { get; } + + public string this[string key] + { + get + { + if (string.IsNullOrEmpty(key)) + { + return key ?? string.Empty; + } + + string normalized = key.Trim(); + return _strings.TryGetValue(normalized, out string? value) ? value : key; + } + } + + public string Format(string key, params object[] args) + => string.Format(Culture, this[key], args ?? Array.Empty()); + + public static CsvLocalizationService CreateDefault(CultureInfo? culture = null) + { + CultureInfo effectiveCulture = culture ?? CultureInfo.CurrentCulture; + Logger.Info("Current language is: {0}", effectiveCulture.Name); + return new CsvLocalizationService(Shadowsocks.Core.EmbeddedResources.I18nCsv, effectiveCulture); + } + + private void Load(string csv, string locale) + { + using var parser = new TextFieldParser(new StringReader(csv)); + parser.SetDelimiters(","); + parser.HasFieldsEnclosedInQuotes = true; + + string[]? localeNames = parser.ReadFields(); + if (localeNames is null || localeNames.Length == 0) + { + return; + } + + int enIndex = Array.FindIndex(localeNames, value => string.Equals(value, "en", StringComparison.OrdinalIgnoreCase)); + if (enIndex < 0) + { + enIndex = 0; + } + + int targetIndex = Array.FindIndex(localeNames, value => string.Equals(value, locale, StringComparison.OrdinalIgnoreCase)); + if (targetIndex < 0) + { + string language = locale.Split('-')[0]; + targetIndex = Array.FindIndex(localeNames, value => + string.Equals(value.Split('-')[0], language, StringComparison.OrdinalIgnoreCase)); + + if (targetIndex >= 0 && targetIndex != enIndex) + { + Logger.Info("Using {0} translation for {1}", localeNames[targetIndex], locale); + } + else if (targetIndex < 0) + { + Logger.Info("Translation for {0} not found; English keys will be used.", locale); + return; + } + } + + while (!parser.EndOfData) + { + string[]? row = parser.ReadFields(); + if (row is null || row.Length <= enIndex) + { + continue; + } + + string source = row[enIndex].Trim(); + if (string.IsNullOrWhiteSpace(source) || source.StartsWith('#')) + { + continue; + } + + if (row.Length <= targetIndex) + { + continue; + } + + string translation = row[targetIndex].Trim(); + if (!string.IsNullOrWhiteSpace(translation)) + { + _strings[source] = translation; + } + } + } +} diff --git a/Shadowsocks.Core/Localization/ILocalizationService.cs b/Shadowsocks.Core/Localization/ILocalizationService.cs new file mode 100644 index 000000000..ad9eb2bce --- /dev/null +++ b/Shadowsocks.Core/Localization/ILocalizationService.cs @@ -0,0 +1,12 @@ +using System.Globalization; + +namespace Shadowsocks.Localization; + +public interface ILocalizationService +{ + CultureInfo Culture { get; } + + string this[string key] { get; } + + string Format(string key, params object[] args); +} diff --git a/Shadowsocks.Core/Logging/LoggingConfigurator.cs b/Shadowsocks.Core/Logging/LoggingConfigurator.cs new file mode 100644 index 000000000..b44853595 --- /dev/null +++ b/Shadowsocks.Core/Logging/LoggingConfigurator.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using NLog; +using NLog.Config; +using NLog.Targets; +using Shadowsocks.Core.Settings; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Core.Logging; + +public static class LoggingConfigurator +{ + private static readonly object SyncRoot = new(); + private static string activeLogFilePath; + + public static string LogFilePath => activeLogFilePath ?? ResolveLogFilePath(AppSettings.Logging.FilePath); + + public static void Configure(bool verbose = false) + { + lock (SyncRoot) + { + LoggingSettings settings = AppSettings.Logging; + string logFilePath = PrepareLogFilePath(settings); + activeLogFilePath = logFilePath; + + LogLevel minimumLevel = ParseLevel( + verbose ? settings.VerboseMinimumLevel : settings.MinimumLevel, + verbose ? LogLevel.Debug : LogLevel.Info); + + var fileTarget = new FileTarget("file") + { + FileName = logFilePath, + Layout = settings.Layout, + ArchiveAboveSize = Math.Max(1024L * 1024L, settings.ArchiveAboveSizeBytes), + ArchiveEvery = FileArchivePeriod.Day, + ArchiveSuffixFormat = "_{1:yyyyMMdd}_{0}", + MaxArchiveFiles = Math.Max(1, settings.MaxArchiveFiles), + MaxArchiveDays = Math.Max(1, settings.MaxArchiveDays), + }; + + var configuration = new LoggingConfiguration(); + configuration.AddRule(minimumLevel, LogLevel.Fatal, fileTarget); + LogManager.Configuration = configuration; + LogManager.ReconfigExistingLoggers(); + } + } + + + private static string PrepareLogFilePath(LoggingSettings settings) + { + string primaryPath = ResolveLogFilePath(settings.FilePath); + try + { + EnsureLogDirectory(primaryPath); + return primaryPath; + } + catch (UnauthorizedAccessException) + { + return PrepareFallbackLogFilePath(settings.FallbackFilePath); + } + catch (IOException) + { + return PrepareFallbackLogFilePath(settings.FallbackFilePath); + } + } + + private static string PrepareFallbackLogFilePath(string configuredPath) + { + string fallback = string.IsNullOrWhiteSpace(configuredPath) + ? AppStoragePaths.LogFile + : Environment.ExpandEnvironmentVariables(configuredPath.Trim()); + string fallbackPath = Path.GetFullPath(fallback); + EnsureLogDirectory(fallbackPath); + return fallbackPath; + } + + private static void EnsureLogDirectory(string logFilePath) + { + string logDirectory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrWhiteSpace(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + } + + private static string ResolveLogFilePath(string configuredPath) + { + string path = string.IsNullOrWhiteSpace(configuredPath) + ? AppStoragePaths.LogFile + : Environment.ExpandEnvironmentVariables(configuredPath.Trim()); + + return Path.IsPathRooted(path) + ? Path.GetFullPath(path) + : Path.GetFullPath(Path.Combine(AppStoragePaths.LogsDirectory, path)); + } + + private static LogLevel ParseLevel(string value, LogLevel fallback) + { + return value?.Trim().ToUpperInvariant() switch + { + "TRACE" => LogLevel.Trace, + "DEBUG" => LogLevel.Debug, + "INFO" => LogLevel.Info, + "WARN" or "WARNING" => LogLevel.Warn, + "ERROR" => LogLevel.Error, + "FATAL" => LogLevel.Fatal, + _ => fallback, + }; + } +} diff --git a/Shadowsocks.Core/Model/Configuration.cs b/Shadowsocks.Core/Model/Configuration.cs new file mode 100644 index 000000000..d43ac1145 --- /dev/null +++ b/Shadowsocks.Core/Model/Configuration.cs @@ -0,0 +1,662 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Newtonsoft.Json; +using NLog; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; +using Shadowsocks.Core; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Model +{ + [Serializable] + public class Configuration + { + [JsonIgnore] + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public string version { get; set; } + + public List configs { get; set; } + + public List onlineConfigSource { get; set; } + + // when strategy is set, index is ignored + public string strategy { get; set; } + public int index { get; set; } + public bool global { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + public bool shareOverLan { get; set; } + public bool firstRun { get; set; } + public int localPort { get; set; } + public bool showPluginOutput { get; set; } + public bool showDnsLogs { get; set; } + public string pacUrl { get; set; } + + public bool useOnlinePac { get; set; } + public bool secureLocalPac { get; set; } // enable secret for PAC server + public bool regeneratePacOnUpdate { get; set; } // regenerate pac.txt on version update + public bool autoCheckUpdate { get; set; } + public bool checkPreRelease { get; set; } + public string skippedUpdateVersion { get; set; } // skip the update with this version number + public bool isVerboseLogging { get; set; } + public string uiTheme { get; set; } // System, Light, or Dark. Presentation preference shared by UI shells. + + // hidden options + public bool isIPv6Enabled { get; set; } // for experimental ipv6 support + // GeoSite sources are used only by Local PAC. Every source is cached independently + // and all successfully loaded databases are merged. The checksum URL is derived as + // .sha256sum; checksum absence is non-fatal. + public List geositeUrls { get; set; } + + public List geositeDirectGroups { get; set; } // groups of domains that we connect without the proxy + public List geositeProxiedGroups { get; set; } // groups of domains that we connect via the proxy + public bool geositePreferDirect { get; set; } // a.k.a blacklist mode + public string userAgent { get; set; } + + public LogViewerConfig logViewer { get; set; } + public ForwardProxyConfig proxy { get; set; } + public HotkeyConfig hotkey { get; set; } + + // Traffic capture/routing. User Mode requires no elevation and can identify + // applications that connect to the managed local HTTP proxy. Admin Mode is + // implemented by the optional elevated WinDivert helper. + public TrafficCaptureMode trafficCaptureMode { get; set; } + public List applicationRules { get; set; } + public DnsPolicyConfig dnsPolicy { get; set; } + public List gameModeApplications { get; set; } + + [JsonIgnore] + public bool firstRunOnNewVersion { get; set; } + + public Configuration() + { + version = ApplicationInfo.Version; + strategy = ""; + index = -1; + global = false; + Enabled = false; + shareOverLan = false; + firstRun = true; + localPort = 1080; + showPluginOutput = false; + showDnsLogs = false; + pacUrl = ""; + useOnlinePac = false; + secureLocalPac = true; + regeneratePacOnUpdate = true; + autoCheckUpdate = true; + checkPreRelease = false; + skippedUpdateVersion = ""; + isVerboseLogging = false; + uiTheme = "System"; + + // hidden options + isIPv6Enabled = false; + geositeUrls = new List() + { + GeositeUpdater.DefaultSourceUrl, + }; + geositeDirectGroups = new List() + { + "private", + "cn", + "geolocation-!cn@cn", + }; + geositeProxiedGroups = new List() + { + "geolocation-!cn", + }; + geositePreferDirect = false; + userAgent = "Shadowsocks-Reborn/$version"; + + logViewer = new LogViewerConfig(); + proxy = new ForwardProxyConfig(); + hotkey = new HotkeyConfig(); + + trafficCaptureMode = TrafficCaptureMode.User; + applicationRules = new List(); + dnsPolicy = new DnsPolicyConfig(); + gameModeApplications = new List(); + + firstRunOnNewVersion = false; + + configs = new List(); + onlineConfigSource = new List(); + } + + [JsonIgnore] + public string userAgentString { get; set; } // $version substituted with numeral version in it + + public const string SettingsValueName = "Configuration"; + public const string SettingsBackupValueName = "ConfigurationBackup"; + public const string SettingsSchemaVersionName = "SchemaVersion"; + public const int SettingsSchemaVersion = 3; + private static ISettingsStore settingsStore; + + public static void ConfigureSettingsStore(ISettingsStore store) + { + ArgumentNullException.ThrowIfNull(store); + settingsStore = store; + } + [JsonIgnore] + public string LocalHost => isIPv6Enabled ? "[::1]" : "127.0.0.1"; + + public Server GetCurrentServer() + { + if (index >= 0 && index < configs.Count) + return configs[index]; + else + return GetDefaultServer(); + } + + public WebProxy WebProxy => Enabled + ? new WebProxy( + isIPv6Enabled + ? $"[{IPAddress.IPv6Loopback}]" + : IPAddress.Loopback.ToString(), + localPort) + : null; + + [JsonIgnore] + public bool HasConfiguredServer + { + get + { + if (configs == null || configs.Count == 0) + return false; + + if (!string.IsNullOrWhiteSpace(strategy)) + return configs.Any(server => server?.IsConfigured == true); + + return GetCurrentServer()?.IsConfigured == true; + } + } + + /// + /// Used by multiple forms to validate a server. + /// Communication is done by throwing exceptions. + /// + /// + public static void CheckServer(Server server) + { + CheckServer(server.server); + CheckPort(server.ServerPort); + CheckPassword(server.password); + CheckTimeout(server.timeout, Server.MaxServerTimeoutSec); + } + + /// + /// Loads the user configuration from the configured settings store. Product builds + /// use a JSON file under the active storage root (LocalAppData in normal mode, + /// a disposable Temp session in Clean Mode). + /// + public static Configuration Load() + { + if (settingsStore == null) + { + logger.Warn("No settings store is configured; using default configuration in memory."); + return new Configuration(); + } + + if (!settingsStore.TryGetString(SettingsValueName, out string configContent) || string.IsNullOrWhiteSpace(configContent)) + { + return new Configuration(); + } + + if (TryDeserialize(configContent, out Configuration config)) + { + MigrateSettingsSchemaIfNeeded(config); + return config; + } + + logger.Error("Primary configuration is invalid; attempting the rollback snapshot."); + if (settingsStore.TryGetString(SettingsBackupValueName, out string backupContent) + && TryDeserialize(backupContent, out config)) + { + try + { + settingsStore.SetString(SettingsValueName, backupContent); + } + catch (Exception restoreException) + { + logger.LogUsefulException(restoreException); + } + MigrateSettingsSchemaIfNeeded(config); + return config; + } + + logger.Error("No valid configuration snapshot is available; using defaults."); + return new Configuration(); + } + + public static bool TryDeserialize(string json, out Configuration config) + { + config = null; + if (string.IsNullOrWhiteSpace(json)) + return false; + + try + { + config = JsonConvert.DeserializeObject(json, new JsonSerializerSettings() + { + ObjectCreationHandling = ObjectCreationHandling.Replace + }) ?? new Configuration(); + return true; + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + return false; + } + } + + /// + /// Process the loaded configurations and set up things. + /// + /// A reference of Configuration object. + public static void Process(ref Configuration config) + { + config.configs ??= new List(); + config.onlineConfigSource ??= new List(); + NormalizeGeositeSources(config); + config.applicationRules ??= new List(); + config.logViewer ??= new LogViewerConfig(); + config.proxy ??= new ForwardProxyConfig(); + config.hotkey ??= new HotkeyConfig(); + config.userAgent ??= "Shadowsocks-Reborn/$version"; + config.version ??= "0.0.0.0"; + config.dnsPolicy ??= new DnsPolicyConfig(); + config.dnsPolicy.dnsCrypt ??= new DnsCryptConfig(); + config.dnsPolicy.dnsCrypt.serverNames ??= new List(); + // Persisted DNSCrypt preferences are user state. Do not rewrite legacy/custom + // values here; effective Administrator-mode DNSCrypt interception remains + // fail-closed independently in the NetworkService request contract. + if (config.dnsPolicy.dnsCrypt.automaticResolvers) + config.dnsPolicy.dnsCrypt.serverNames.Clear(); + config.gameModeApplications ??= new List(); + config.uiTheme = NormalizeUiThemePreference(config.uiTheme); + // Mark the first run of a newer product version. Malformed/missing version + // metadata must never prevent the application from starting. + var appVersion = new Version(ApplicationInfo.Version); + if (!Version.TryParse(config.version, out Version configVersion)) + { + configVersion = new Version(0, 0, 0, 0); + } + if (appVersion.CompareTo(configVersion) > 0) + { + config.firstRunOnNewVersion = true; + } + // Empty placeholder servers were historically persisted to keep a synthetic + // "Server 1" row alive. Zero configured servers is now a first-class state. + RemovePersistedEmptyServers(config); + EnsureServerNames(config.configs); + + // Selected server. With no servers there is deliberately no selection and the + // system proxy is disabled so Windows is never pointed at an inactive relay. + if (config.configs.Count == 0) + { + config.index = -1; + config.strategy = string.Empty; + config.Enabled = false; + } + else + { + if (config.index < 0 && string.IsNullOrEmpty(config.strategy)) + config.index = 0; + if (config.index >= config.configs.Count) + config.index = config.configs.Count - 1; + + if (!config.HasConfiguredServer) + { + // Preserve the user's DNS preference. Dependent runtime/UI paths are + // gated by HasConfiguredServer and remain inactive until a server exists. + config.Enabled = false; + } + } + // Check OS IPv6 support + if (!System.Net.Sockets.Socket.OSSupportsIPv6) + config.isIPv6Enabled = false; + config.proxy.CheckConfig(); + // Replace $version with the version number. + config.userAgentString = config.userAgent.Replace("$version", config.version); + + } + + /// + /// Saves the configuration to the configured settings store. The previous valid + /// value is preserved as a rollback snapshot before the primary value is replaced. + /// + public static void Save(Configuration config) + { + ArgumentNullException.ThrowIfNull(config); + if (settingsStore == null) + throw new InvalidOperationException("The settings store must be configured before saving configuration."); + + config.configs ??= new List(); + RemovePersistedEmptyServers(config); + EnsureServerNames(config.configs); + + Server selectedServer = null; + if (string.IsNullOrEmpty(config.strategy) + && config.index >= 0 + && config.index < config.configs.Count) + { + selectedServer = config.configs[config.index]; + } + + config.configs = SortByOnlineConfig(config.configs); + if (config.configs.Count == 0) + { + config.index = -1; + config.strategy = string.Empty; + } + else if (string.IsNullOrEmpty(config.strategy)) + { + int sortedIndex = selectedServer is null ? -1 : config.configs.FindIndex(server => ReferenceEquals(server, selectedServer)); + config.index = sortedIndex >= 0 + ? sortedIndex + : Math.Clamp(config.index, 0, config.configs.Count - 1); + } + if (!config.HasConfiguredServer) + { + // Persist the selected DNS policy even while no server is available. The + // controller applies inactive runtime behavior until a server exists. + config.Enabled = false; + } + string jsonString = SerializeConfiguration(config); + + try + { + if (settingsStore.TryGetString(SettingsValueName, out string previous) + && !string.IsNullOrWhiteSpace(previous) + && TryDeserialize(previous, out _)) + { + settingsStore.SetString(SettingsBackupValueName, previous); + } + + settingsStore.SetString(SettingsValueName, jsonString); + settingsStore.SetInt32(SettingsSchemaVersionName, SettingsSchemaVersion); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + throw; + } + } + + private static void MigrateSettingsSchemaIfNeeded(Configuration config) + { + if (settingsStore is null) + { + return; + } + + int schemaVersion = settingsStore.TryGetInt32(SettingsSchemaVersionName, out int storedVersion) + ? storedVersion + : 0; + if (schemaVersion >= SettingsSchemaVersion) + { + return; + } + + try + { + // Schema v3 removes the historical synthetic empty server without changing + // any persisted DNSCrypt preferences. New DNSCrypt defaults come only from + // constructing a new DnsCryptConfig. + RemovePersistedEmptyServers(config); + + // Re-serializing the typed model is the schema migration boundary: properties + // no longer represented by Configuration are removed without carrying a list + // of historical field names forever. Keep a valid rollback snapshot canonical too. + settingsStore.SetString(SettingsValueName, SerializeConfiguration(config)); + + if (settingsStore.TryGetString(SettingsBackupValueName, out string backupJson) + && TryDeserialize(backupJson, out Configuration backupConfig)) + { + RemovePersistedEmptyServers(backupConfig); + settingsStore.SetString(SettingsBackupValueName, SerializeConfiguration(backupConfig)); + } + + settingsStore.SetInt32(SettingsSchemaVersionName, SettingsSchemaVersion); + } + catch (Exception exception) + { + // Migration failure must not prevent startup; the in-memory configuration is + // already valid and a later successful save will persist the current schema. + logger.LogUsefulException(exception); + } + } + + private static void RemovePersistedEmptyServers(Configuration config) + { + config.configs ??= new List(); + int selectedIndex = config.index; + int removedBeforeSelection = 0; + bool selectedWasRemoved = false; + + for (int index = config.configs.Count - 1; index >= 0; index--) + { + if (!IsPersistedEmptyServer(config.configs[index])) + continue; + + if (selectedIndex >= 0) + { + if (index < selectedIndex) + removedBeforeSelection++; + else if (index == selectedIndex) + selectedWasRemoved = true; + } + + config.configs.RemoveAt(index); + } + + if (config.configs.Count == 0) + { + config.index = -1; + return; + } + + if (!string.IsNullOrEmpty(config.strategy)) + return; + + if (selectedIndex < 0) + { + config.index = 0; + return; + } + + int adjustedIndex = selectedIndex - removedBeforeSelection; + if (selectedWasRemoved && adjustedIndex >= config.configs.Count) + adjustedIndex = config.configs.Count - 1; + config.index = Math.Clamp(adjustedIndex, 0, config.configs.Count - 1); + } + + private static bool IsPersistedEmptyServer(Server server) + => server is null + || (string.IsNullOrWhiteSpace(server.server) + && string.IsNullOrWhiteSpace(server.password) + && string.IsNullOrWhiteSpace(server.remarks) + && string.IsNullOrWhiteSpace(server.plugin) + && string.IsNullOrWhiteSpace(server.PluginOptions) + && string.IsNullOrWhiteSpace(server.PluginArguments) + && string.IsNullOrWhiteSpace(server.group)); + + private static string SerializeConfiguration(Configuration config) + => JsonConvert.SerializeObject(config, Formatting.Indented); + + /// + /// Gives every configured server a stable display name when the source did not + /// provide one. Explicit names from ss:// fragments, subscriptions, or the user + /// are preserved. Generated names are unique within the current configuration. + /// + public static void EnsureServerNames(IList servers) + { + ArgumentNullException.ThrowIfNull(servers); + + var usedNames = new HashSet( + servers + .Where(server => server is not null && !string.IsNullOrWhiteSpace(server.remarks)) + .Select(server => server.remarks.Trim()), + StringComparer.OrdinalIgnoreCase); + + int sequence = 1; + foreach (Server server in servers) + { + if (server is null || !server.IsConfigured || !string.IsNullOrWhiteSpace(server.remarks)) + { + continue; + } + + string candidate; + do + { + candidate = $"Server {sequence++}"; + } + while (!usedNames.Add(candidate)); + + server.remarks = candidate; + } + } + + public static List SortByOnlineConfig(IEnumerable servers) + { + var groups = servers.GroupBy(s => s.group); + List ret = new List(); + ret.AddRange(groups.Where(g => string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); + ret.AddRange(groups.Where(g => !string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); + return ret; + } + + public static List NormalizeGeositeSourceList(IEnumerable sources) + { + var normalized = (sources ?? Enumerable.Empty()) + .Select(source => source?.Trim()) + .Where(source => !string.IsNullOrWhiteSpace(source)) + .Where(source => Uri.TryCreate(source, UriKind.Absolute, out Uri uri) && + (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (normalized.Count == 0) + normalized.Add(GeositeUpdater.DefaultSourceUrl); + + return normalized; + } + + public static string NormalizeUiThemePreference(string theme) + { + return theme?.Trim().ToUpperInvariant() switch + { + "LIGHT" => "Light", + "DARK" => "Dark", + _ => "System", + }; + } + + private static void NormalizeGeositeSources(Configuration config) + { + if (config == null) + return; + + config.geositeUrls = NormalizeGeositeSourceList(config.geositeUrls ?? new List()); + } + + /// + /// Validates if the groups in the list are all valid. + /// + /// The list of groups to validate. + /// + /// True if all groups are valid. + /// False if any one of them is invalid. + /// + public static bool ValidateGeositeGroupList(List groups) + { + foreach (var geositeGroup in groups) + if (!GeositeUpdater.CheckGeositeGroup(geositeGroup)) // found invalid group + { +#if DEBUG + logger.Debug($"Available groups:"); + foreach (var group in GeositeUpdater.Geosites.Keys) + logger.Debug($"{group}"); +#endif + logger.Warn($"The Geosite group {geositeGroup} doesn't exist. Resetting to default groups."); + return false; + } + return true; + } + + public static void ResetGeositeDirectGroup(ref List geositeDirectGroups) + { + geositeDirectGroups.Clear(); + geositeDirectGroups.Add("private"); + geositeDirectGroups.Add("cn"); + geositeDirectGroups.Add("geolocation-!cn@cn"); + } + + public static void ResetGeositeProxiedGroup(ref List geositeProxiedGroups) + { + geositeProxiedGroups.Clear(); + geositeProxiedGroups.Add("geolocation-!cn"); + } + + public static void ResetUserAgent(Configuration config) + { + config.userAgent = "Shadowsocks-Reborn/$version"; + config.userAgentString = config.userAgent.Replace("$version", config.version); + } + + public static Server AddDefaultServerOrServer(Configuration config, Server server = null, int? index = null) + { + if (config?.configs != null) + { + server = (server ?? GetDefaultServer()); + + config.configs.Insert(index.GetValueOrDefault(config.configs.Count), server); + } + return server; + } + + public static Server GetDefaultServer() + { + return new Server(); + } + + public static void CheckPort(int port) + { + if (port <= 0 || port > 65535) + throw new ArgumentException(I18N.GetString("Port out of range")); + } + + public static void CheckLocalPort(int port) + { + CheckPort(port); + if (port == 8123) + throw new ArgumentException(I18N.GetString("Port can't be 8123")); + } + + private static void CheckPassword(string password) + { + if (string.IsNullOrEmpty(password)) + throw new ArgumentException(I18N.GetString("Password can not be blank")); + } + + public static void CheckServer(string server) + { + if (string.IsNullOrEmpty(server)) + throw new ArgumentException(I18N.GetString("Server IP can not be blank")); + } + + public static void CheckTimeout(int timeout, int maxTimeout) + { + if (timeout <= 0 || timeout > maxTimeout) + throw new ArgumentException( + I18N.GetString("Timeout is invalid, it should not exceed {0}", maxTimeout)); + } + } +} diff --git a/shadowsocks-csharp/Model/ForwardProxyConfig.cs b/Shadowsocks.Core/Model/ForwardProxyConfig.cs similarity index 50% rename from shadowsocks-csharp/Model/ForwardProxyConfig.cs rename to Shadowsocks.Core/Model/ForwardProxyConfig.cs index 7a1d4ec51..907ae8284 100644 --- a/shadowsocks-csharp/Model/ForwardProxyConfig.cs +++ b/Shadowsocks.Core/Model/ForwardProxyConfig.cs @@ -5,25 +5,25 @@ namespace Shadowsocks.Model [Serializable] public class ForwardProxyConfig { - public const int PROXY_SOCKS5 = 0; - public const int PROXY_HTTP = 1; + public const int ProxySocks5 = 0; + public const int ProxyHttp = 1; public const int MaxProxyTimeoutSec = 10; private const int DefaultProxyTimeoutSec = 3; - public bool useProxy; - public int proxyType; - public string proxyServer; - public int proxyPort; - public int proxyTimeout; - public bool useAuth; - public string authUser; - public string authPwd; + public bool useProxy { get; set; } + public int proxyType { get; set; } + public string proxyServer { get; set; } + public int proxyPort { get; set; } + public int proxyTimeout { get; set; } + public bool useAuth { get; set; } + public string authUser { get; set; } + public string authPwd { get; set; } public ForwardProxyConfig() { useProxy = false; - proxyType = PROXY_SOCKS5; + proxyType = ProxySocks5; proxyServer = ""; proxyPort = 0; proxyTimeout = DefaultProxyTimeoutSec; @@ -34,9 +34,9 @@ public ForwardProxyConfig() public void CheckConfig() { - if (proxyType < PROXY_SOCKS5 || proxyType > PROXY_HTTP) + if (proxyType < ProxySocks5 || proxyType > ProxyHttp) { - proxyType = PROXY_SOCKS5; + proxyType = ProxySocks5; } } } diff --git a/shadowsocks-csharp/Model/Geosite/Geosite.cs b/Shadowsocks.Core/Model/Geosite/Geosite.cs similarity index 99% rename from shadowsocks-csharp/Model/Geosite/Geosite.cs rename to Shadowsocks.Core/Model/Geosite/Geosite.cs index 731ea4d86..fe2a93e21 100644 --- a/shadowsocks-csharp/Model/Geosite/Geosite.cs +++ b/Shadowsocks.Core/Model/Geosite/Geosite.cs @@ -1,8 +1,8 @@ -// +// // Generated by the protocol buffer compiler. DO NOT EDIT! // source: geosite.proto // -#pragma warning disable 1591, 0612, 3021 +#pragma warning disable 1591, 0612, 3021, 8981 #region Designer generated code using pb = global::Google.Protobuf; diff --git a/shadowsocks-csharp/Model/Geosite/geosite.proto b/Shadowsocks.Core/Model/Geosite/geosite.proto similarity index 100% rename from shadowsocks-csharp/Model/Geosite/geosite.proto rename to Shadowsocks.Core/Model/Geosite/geosite.proto diff --git a/shadowsocks-csharp/Model/HotKeyConfig.cs b/Shadowsocks.Core/Model/HotKeyConfig.cs similarity index 56% rename from shadowsocks-csharp/Model/HotKeyConfig.cs rename to Shadowsocks.Core/Model/HotKeyConfig.cs index 4e27a08b4..eda429a35 100644 --- a/shadowsocks-csharp/Model/HotKeyConfig.cs +++ b/Shadowsocks.Core/Model/HotKeyConfig.cs @@ -1,33 +1,33 @@ -using System; - -namespace Shadowsocks.Model -{ - /* - * Format: - * + - * - */ - - [Serializable] - public class HotkeyConfig - { - public string SwitchSystemProxy; - public string SwitchSystemProxyMode; - public string SwitchAllowLan; - public string ShowLogs; - public string ServerMoveUp; - public string ServerMoveDown; - public bool RegHotkeysAtStartup; - - public HotkeyConfig() - { - SwitchSystemProxy = ""; - SwitchSystemProxyMode = ""; - SwitchAllowLan = ""; - ShowLogs = ""; - ServerMoveUp = ""; - ServerMoveDown = ""; - RegHotkeysAtStartup = false; - } - } -} \ No newline at end of file +using System; + +namespace Shadowsocks.Model +{ + /* + * Format: + * + + * + */ + + [Serializable] + public class HotkeyConfig + { + public string SwitchSystemProxy { get; set; } + public string SwitchSystemProxyMode { get; set; } + public string SwitchAllowLan { get; set; } + public string ShowLogs { get; set; } + public string ServerMoveUp { get; set; } + public string ServerMoveDown { get; set; } + public bool RegHotkeysAtStartup { get; set; } + + public HotkeyConfig() + { + SwitchSystemProxy = ""; + SwitchSystemProxyMode = ""; + SwitchAllowLan = ""; + ShowLogs = ""; + ServerMoveUp = ""; + ServerMoveDown = ""; + RegHotkeysAtStartup = false; + } + } +} diff --git a/Shadowsocks.Core/Model/LogViewerConfig.cs b/Shadowsocks.Core/Model/LogViewerConfig.cs new file mode 100644 index 000000000..2db8b6b3f --- /dev/null +++ b/Shadowsocks.Core/Model/LogViewerConfig.cs @@ -0,0 +1,35 @@ +using System; + +namespace Shadowsocks.Model +{ + /// + /// Platform-neutral persisted state for the log viewer. UI-specific Font/Color + /// objects are materialized only by the presentation layer. + /// + [Serializable] + public class LogViewerConfig + { + public bool topMost { get; set; } + public bool wrapText { get; set; } + public bool toolbarShown { get; set; } + public int width { get; set; } = 600; + public int height { get; set; } = 400; + public int top { get; set; } + public int left { get; set; } + public bool maximized { get; set; } = true; + + public string fontFamily { get; set; } = "Consolas"; + public float fontSize { get; set; } = 8F; + public int fontStyle { get; set; } + public int backgroundArgb { get; set; } = unchecked((int)0xFF000000); + public int textArgb { get; set; } = unchecked((int)0xFFFFFFFF); + + public LogViewerConfig() + { + topMost = false; + wrapText = false; + toolbarShown = true; + } + + } +} diff --git a/Shadowsocks.Core/Model/Server.cs b/Shadowsocks.Core/Model/Server.cs new file mode 100644 index 000000000..2dc54c33f --- /dev/null +++ b/Shadowsocks.Core/Model/Server.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Text; +using Newtonsoft.Json; +using Shadowsocks.Controller; + +namespace Shadowsocks.Model +{ + [Serializable] + public class Server + { + public const string DefaultMethod = "chacha20-ietf-poly1305"; + public const int DefaultPort = 8388; + + private const int DefaultServerTimeoutSec = 5; + public const int MaxServerTimeoutSec = 20; + + public string server { get; set; } + [JsonProperty("server_port")] + public int ServerPort { get; set; } + public string password { get; set; } + public string method { get; set; } + // optional fields + [DefaultValue("")] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public string plugin { get; set; } + [DefaultValue("")] + [JsonProperty("plugin_opts", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public string PluginOptions { get; set; } + [DefaultValue("")] + [JsonProperty("plugin_args", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public string PluginArguments { get; set; } + [DefaultValue("")] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public string remarks { get; set; } + + [DefaultValue("")] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public string group { get; set; } + + public int timeout { get; set; } + + // Credentials originating from a link/subscription stay reveal-protected in the UI. + // The flag is persisted so a direct SIP002 import remains protected after restart. + [DefaultValue(false)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + public bool importedFromUrl { get; set; } + + [JsonIgnore] + public bool IsConfigured => !string.IsNullOrWhiteSpace(server) + && ServerPort > 0 + && ServerPort <= 65535 + && !string.IsNullOrEmpty(password) + && !string.IsNullOrWhiteSpace(method) + && timeout > 0 + && timeout <= MaxServerTimeoutSec; + + public override int GetHashCode() + { + return StringComparer.Ordinal.GetHashCode(server ?? string.Empty) ^ ServerPort; + } + + public override bool Equals(object obj) => obj is Server o2 && server == o2.server && ServerPort == o2.ServerPort; + + public override string ToString() + { + if (string.IsNullOrEmpty(server)) + { + return I18N.GetString("New server"); + } + + string serverStr = $"{FormalHostName}:{ServerPort}"; + return string.IsNullOrEmpty(remarks) + ? serverStr + : $"{remarks} ({serverStr})"; + } + + public string GetURL() + { + string parts = $"{method}:{password}"; + string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); + string websafeBase64 = base64.Replace('+', '-').Replace('/', '_').TrimEnd('='); + + string url = $"{websafeBase64}@{FormalHostName}:{ServerPort}/"; + + if (!string.IsNullOrWhiteSpace(plugin)) + { + string pluginPart = plugin; + if (!string.IsNullOrWhiteSpace(PluginOptions)) + { + pluginPart += ";" + PluginOptions; + } + url += "?plugin=" + UrlEncodeShareComponent(pluginPart); + } + + string tag = string.IsNullOrEmpty(remarks) + ? string.Empty + : $"#{UrlEncodeShareComponent(remarks)}"; + return $"ss://{url}{tag}"; + } + + private static string UrlEncodeShareComponent(string value) + { + string encoded = WebUtility.UrlEncode(value); + int percentIndex = encoded.IndexOf('%'); + if (percentIndex < 0) + { + return encoded; + } + + char[] chars = encoded.ToCharArray(); + for (int i = percentIndex; i + 2 < chars.Length; i++) + { + if (chars[i] != '%') + { + continue; + } + + // Keep the historical Shadowsocks-Windows share-URL form. + // Modern .NET emits uppercase hexadecimal digits in percent escapes, + // while existing links/tests use lowercase escapes (%3b, %3d, ...). + chars[i + 1] = char.ToLowerInvariant(chars[i + 1]); + chars[i + 2] = char.ToLowerInvariant(chars[i + 2]); + i += 2; + } + + return new string(chars); + } + + [JsonIgnore] + public string FormalHostName + { + get + { + // CheckHostName() won't do a real DNS lookup + switch (Uri.CheckHostName(server)) + { + case UriHostNameType.IPv6: // Add square bracket when IPv6 (RFC3986) + return $"[{server}]"; + default: // IPv4 or domain name + return server; + } + } + } + + public Server() + { + server = ""; + ServerPort = DefaultPort; + method = DefaultMethod; + plugin = ""; + PluginOptions = ""; + PluginArguments = ""; + password = ""; + remarks = ""; + timeout = DefaultServerTimeoutSec; + } + + public static Server ParseURL(string serverUrl) + { + if (string.IsNullOrWhiteSpace(serverUrl)) + { + return null; + } + + string normalized = serverUrl.Trim(); + if (!normalized.StartsWith("ss://", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + Uri parsedUrl; + try + { + parsedUrl = new Uri(normalized); + } + catch (UriFormatException) + { + return null; + } + + if (string.IsNullOrWhiteSpace(parsedUrl.IdnHost) || parsedUrl.Port <= 0) + { + return null; + } + + string rawUserInfo = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); + string base64 = rawUserInfo.Replace('-', '+').Replace('_', '/'); + string userInfo; + try + { + userInfo = Encoding.UTF8.GetString(Convert.FromBase64String( + base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='))); + } + catch (FormatException) + { + return null; + } + + string[] userInfoParts = userInfo.Split(':', 2); + if (userInfoParts.Length != 2) + { + return null; + } + + var server = new Server + { + remarks = WebUtility.UrlDecode(parsedUrl.GetComponents(UriComponents.Fragment, UriFormat.Unescaped)), + server = parsedUrl.IdnHost, + ServerPort = parsedUrl.Port, + method = userInfoParts[0], + password = userInfoParts[1], + importedFromUrl = true, + }; + + string pluginValue = GetQueryParameter(parsedUrl, "plugin") ?? string.Empty; + string[] pluginParts = pluginValue.Split(';', 2); + server.plugin = pluginParts.Length > 0 ? pluginParts[0] ?? string.Empty : string.Empty; + server.PluginOptions = pluginParts.Length > 1 ? pluginParts[1] ?? string.Empty : string.Empty; + return server; + } + + private static string GetQueryParameter(Uri uri, string name) + { + string query = uri?.Query; + if (string.IsNullOrEmpty(query)) return null; + + foreach (string pair in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + string[] parts = pair.Split('=', 2); + string key = WebUtility.UrlDecode(parts[0]); + if (!string.Equals(key, name, StringComparison.OrdinalIgnoreCase)) continue; + return parts.Length == 2 ? WebUtility.UrlDecode(parts[1]) : string.Empty; + } + + return null; + } + + public static List GetServers(string ssURL) + { + return ssURL + .Split('\r', '\n', ' ') + .Select(u => ParseURL(u)) + .Where(s => s != null) + .ToList(); + } + + public string Identifier() + { + return server + ':' + ServerPort; + } + } +} diff --git a/Shadowsocks.Core/Properties/AssemblyInfo.cs b/Shadowsocks.Core/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..90a50a42f --- /dev/null +++ b/Shadowsocks.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("shadowsocks-reborn")] +[assembly: InternalsVisibleTo("Shadowsocks.Windows")] +[assembly: InternalsVisibleTo("Shadowsocks.UnitTests")] diff --git a/shadowsocks-csharp/Proxy/DirectConnect.cs b/Shadowsocks.Core/Proxy/DirectConnect.cs similarity index 92% rename from shadowsocks-csharp/Proxy/DirectConnect.cs rename to Shadowsocks.Core/Proxy/DirectConnect.cs index 8a5e7479d..16a8220e8 100644 --- a/shadowsocks-csharp/Proxy/DirectConnect.cs +++ b/Shadowsocks.Core/Proxy/DirectConnect.cs @@ -6,7 +6,7 @@ namespace Shadowsocks.Proxy { - public class DirectConnect : IProxy + public sealed class DirectConnect : IProxy, IDisposable { private class FakeAsyncResult : IAsyncResult { @@ -16,7 +16,7 @@ public FakeAsyncResult(object state) } public bool IsCompleted { get; } = true; - public WaitHandle AsyncWaitHandle { get; } = null; + public WaitHandle AsyncWaitHandle => null; public object AsyncState { get; } public bool CompletedSynchronously { get; } = true; } @@ -95,5 +95,11 @@ public void Close() { _remote.Dispose(); } + + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } } } diff --git a/Shadowsocks.Core/Proxy/HttpProxy.cs b/Shadowsocks.Core/Proxy/HttpProxy.cs new file mode 100644 index 000000000..a9fe4c86f --- /dev/null +++ b/Shadowsocks.Core/Proxy/HttpProxy.cs @@ -0,0 +1,360 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller; +using Shadowsocks.Core; +using Shadowsocks.Util.Sockets; + +namespace Shadowsocks.Proxy +{ + public sealed class HttpProxy : IProxy, IDisposable + { + private const int MaxProxyResponseHeaderBytes = 64 * 1024; + private const string HttpCrlf = "\r\n"; + + private static readonly Regex HttpStatusLineRegex = new( + @"^HTTP/1\.[01] (?\d{3})(?:\s|$)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly byte[] HeaderTerminator = Encoding.ASCII.GetBytes("\r\n\r\n"); + private static readonly byte[] LineTerminator = Encoding.ASCII.GetBytes(HttpCrlf); + + private readonly WrappedSocket _remote = new(); + private readonly object _pendingReceiveLock = new(); + private byte[] _pendingReceiveBuffer = Array.Empty(); + private int _pendingReceiveOffset; + + public EndPoint LocalEndPoint => _remote.LocalEndPoint; + public EndPoint ProxyEndPoint { get; private set; } + public EndPoint DestEndPoint { get; private set; } + + public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state) + { + ArgumentNullException.ThrowIfNull(remoteEP); + ProxyEndPoint = remoteEP; + _remote.BeginConnect(remoteEP, callback, state); + } + + public void EndConnectProxy(IAsyncResult asyncResult) + { + _remote.EndConnect(asyncResult); + _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + } + + public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null) + { + ArgumentNullException.ThrowIfNull(destEndPoint); + DestEndPoint = destEndPoint; + ClearPendingReceiveBuffer(); + + var completion = new TaskCompletionSource(state, TaskCreationOptions.RunContinuationsAsynchronously); + if (callback != null) + { + completion.Task.ContinueWith( + task => callback(task), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + _ = CompleteConnectDestAsync(destEndPoint, auth, completion); + } + + public void EndConnectDest(IAsyncResult asyncResult) + { + if (asyncResult is not Task task) + { + throw new ArgumentException("Invalid asyncResult.", nameof(asyncResult)); + } + task.GetAwaiter().GetResult(); + } + + public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, + object state) + { + _remote.BeginSend(buffer, offset, size, socketFlags, callback, state); + } + + public int EndSend(IAsyncResult asyncResult) + { + return _remote.EndSend(asyncResult); + } + + public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, + object state) + { + if (TryReadPending(buffer, offset, size, out int copied)) + { + var completion = new TaskCompletionSource(state, TaskCreationOptions.RunContinuationsAsynchronously); + completion.TrySetResult(copied); + if (callback != null) + { + ThreadPool.QueueUserWorkItem(_ => callback(completion.Task)); + } + return; + } + + _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state); + } + + public int EndReceive(IAsyncResult asyncResult) + { + if (asyncResult is Task bufferedReceive) + { + return bufferedReceive.GetAwaiter().GetResult(); + } + return _remote.EndReceive(asyncResult); + } + + public void Shutdown(SocketShutdown how) + { + _remote.Shutdown(how); + } + + public void Close() + { + ClearPendingReceiveBuffer(); + _remote.Dispose(); + } + + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } + + private async Task CompleteConnectDestAsync( + EndPoint destination, + NetworkCredential auth, + TaskCompletionSource completion) + { + try + { + string authority = FormatAuthority(destination); + string authInfo = string.Empty; + if (auth != null) + { + string authKey = Convert.ToBase64String( + Encoding.UTF8.GetBytes((auth.UserName ?? string.Empty) + ":" + (auth.Password ?? string.Empty))); + authInfo = $"Proxy-Authorization: Basic {authKey}{HttpCrlf}"; + } + + string requestText = + $"CONNECT {authority} HTTP/1.1{HttpCrlf}" + + $"Host: {authority}{HttpCrlf}" + + $"Proxy-Connection: keep-alive{HttpCrlf}" + + $"User-Agent: Shadowsocks-Reborn/{ApplicationInfo.Version}{HttpCrlf}" + + authInfo + HttpCrlf; + byte[] request = Encoding.ASCII.GetBytes(requestText); + await SendAllAsync(request).ConfigureAwait(false); + await ReadProxyResponseAsync().ConfigureAwait(false); + completion.TrySetResult(null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + } + + private async Task ReadProxyResponseAsync() + { + using var response = new MemoryStream(); + var chunk = new byte[4096]; + + while (response.Length < MaxProxyResponseHeaderBytes) + { + int allowed = (int)Math.Min(chunk.Length, MaxProxyResponseHeaderBytes - response.Length); + int received = await ReceiveAsync(chunk, 0, allowed).ConfigureAwait(false); + if (received <= 0) + { + throw ProxyRequestFailed(); + } + + response.Write(chunk, 0, received); + byte[] bytes = response.GetBuffer(); + int length = checked((int)response.Length); + int terminatorIndex = IndexOf(bytes, length, HeaderTerminator); + if (terminatorIndex < 0) + { + continue; + } + + int statusLineEnd = IndexOf(bytes, terminatorIndex + LineTerminator.Length, LineTerminator); + if (statusLineEnd <= 0) + { + throw ProxyRequestFailed(); + } + + string statusLine = Encoding.ASCII.GetString(bytes, 0, statusLineEnd); + Match match = HttpStatusLineRegex.Match(statusLine); + if (!match.Success || !string.Equals(match.Groups["status"].Value, "200", StringComparison.Ordinal)) + { + throw ProxyRequestFailed(); + } + + int payloadOffset = terminatorIndex + HeaderTerminator.Length; + int payloadLength = length - payloadOffset; + if (payloadLength > 0) + { + StorePending(bytes, payloadOffset, payloadLength); + } + return; + } + + throw new InvalidDataException("HTTP proxy response headers exceeded the 64 KiB limit."); + } + + private static string FormatAuthority(EndPoint endpoint) + { + return endpoint switch + { + DnsEndPoint dns => $"{new IdnMapping().GetAscii(dns.Host)}:{dns.Port}", + IPEndPoint ip when ip.AddressFamily == AddressFamily.InterNetworkV6 => $"[{ip.Address}]:{ip.Port}", + IPEndPoint ip => $"{ip.Address}:{ip.Port}", + _ => throw new ArgumentException(I18N.GetString("Proxy request failed"), nameof(endpoint)), + }; + } + + private async Task SendAllAsync(byte[] buffer) + { + int offset = 0; + while (offset < buffer.Length) + { + int sent = await SendAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false); + if (sent <= 0) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + offset += sent; + } + } + + private Task SendAsync(byte[] buffer, int offset, int count) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + try + { + _remote.BeginSend(buffer, offset, count, SocketFlags.None, ar => + { + try + { + completion.TrySetResult(_remote.EndSend(ar)); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + return completion.Task; + } + + private Task ReceiveAsync(byte[] buffer, int offset, int count) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + try + { + _remote.BeginReceive(buffer, offset, count, SocketFlags.None, ar => + { + try + { + completion.TrySetResult(_remote.EndReceive(ar)); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + return completion.Task; + } + + private void StorePending(byte[] source, int offset, int count) + { + lock (_pendingReceiveLock) + { + _pendingReceiveBuffer = new byte[count]; + Buffer.BlockCopy(source, offset, _pendingReceiveBuffer, 0, count); + _pendingReceiveOffset = 0; + } + } + + private bool TryReadPending(byte[] destination, int offset, int count, out int copied) + { + copied = 0; + if (count <= 0) + { + return false; + } + + lock (_pendingReceiveLock) + { + int remaining = _pendingReceiveBuffer.Length - _pendingReceiveOffset; + if (remaining <= 0) + { + return false; + } + + copied = Math.Min(count, remaining); + Buffer.BlockCopy(_pendingReceiveBuffer, _pendingReceiveOffset, destination, offset, copied); + _pendingReceiveOffset += copied; + if (_pendingReceiveOffset >= _pendingReceiveBuffer.Length) + { + _pendingReceiveBuffer = Array.Empty(); + _pendingReceiveOffset = 0; + } + return true; + } + } + + private void ClearPendingReceiveBuffer() + { + lock (_pendingReceiveLock) + { + _pendingReceiveBuffer = Array.Empty(); + _pendingReceiveOffset = 0; + } + } + + private static int IndexOf(byte[] haystack, int haystackLength, byte[] needle) + { + if (needle.Length == 0 || haystackLength < needle.Length) + { + return -1; + } + + int lastStart = haystackLength - needle.Length; + for (int start = 0; start <= lastStart; start++) + { + int index = 0; + while (index < needle.Length && haystack[start + index] == needle[index]) + { + index++; + } + if (index == needle.Length) + { + return start; + } + } + return -1; + } + + private static InvalidOperationException ProxyRequestFailed() + { + return new InvalidOperationException(I18N.GetString("Proxy request failed")); + } + } +} diff --git a/shadowsocks-csharp/Proxy/IProxy.cs b/Shadowsocks.Core/Proxy/IProxy.cs similarity index 100% rename from shadowsocks-csharp/Proxy/IProxy.cs rename to Shadowsocks.Core/Proxy/IProxy.cs diff --git a/Shadowsocks.Core/Proxy/Socks5Proxy.cs b/Shadowsocks.Core/Proxy/Socks5Proxy.cs new file mode 100644 index 000000000..771668796 --- /dev/null +++ b/Shadowsocks.Core/Proxy/Socks5Proxy.cs @@ -0,0 +1,360 @@ +using System; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller; +using Shadowsocks.Util.Sockets; + +namespace Shadowsocks.Proxy +{ + public sealed class Socks5Proxy : IProxy, IDisposable + { + private const byte SocksVersion = 0x05; + private const byte AuthVersion = 0x01; + private const byte MethodNoAuthentication = 0x00; + private const byte MethodUsernamePassword = 0x02; + private const byte MethodNoAcceptableMethods = 0xFF; + private const byte CommandConnect = 0x01; + private const byte AddressTypeIpv4 = 0x01; + private const byte AddressTypeDomain = 0x03; + private const byte AddressTypeIpv6 = 0x04; + + private readonly WrappedSocket _remote = new WrappedSocket(); + + public EndPoint LocalEndPoint => _remote.LocalEndPoint; + public EndPoint ProxyEndPoint { get; private set; } + public EndPoint DestEndPoint { get; private set; } + + public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state) + { + ArgumentNullException.ThrowIfNull(remoteEP); + ProxyEndPoint = remoteEP; + _remote.BeginConnect(remoteEP, callback, state); + } + + public void EndConnectProxy(IAsyncResult asyncResult) + { + _remote.EndConnect(asyncResult); + _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + } + + public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null) + { + ArgumentNullException.ThrowIfNull(destEndPoint); + DestEndPoint = destEndPoint; + + var completion = new TaskCompletionSource(state, TaskCreationOptions.RunContinuationsAsynchronously); + if (callback != null) + { + completion.Task.ContinueWith( + task => callback(task), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + _ = CompleteConnectDestAsync(destEndPoint, auth, completion); + } + + public void EndConnectDest(IAsyncResult asyncResult) + { + if (asyncResult is not Task task) + { + throw new ArgumentException("Invalid asyncResult.", nameof(asyncResult)); + } + + task.GetAwaiter().GetResult(); + } + + public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, + object state) + { + _remote.BeginSend(buffer, offset, size, socketFlags, callback, state); + } + + public int EndSend(IAsyncResult asyncResult) + { + return _remote.EndSend(asyncResult); + } + + public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, + object state) + { + _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state); + } + + public int EndReceive(IAsyncResult asyncResult) + { + return _remote.EndReceive(asyncResult); + } + + public void Shutdown(SocketShutdown how) + { + _remote.Shutdown(how); + } + + public void Close() + { + _remote.Dispose(); + } + + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } + + private async Task CompleteConnectDestAsync( + EndPoint destEndPoint, + NetworkCredential auth, + TaskCompletionSource completion) + { + try + { + await NegotiateAuthenticationAsync(auth).ConfigureAwait(false); + await SendConnectRequestAsync(destEndPoint).ConfigureAwait(false); + await ReadConnectReplyAsync().ConfigureAwait(false); + completion.TrySetResult(null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + } + + private async Task NegotiateAuthenticationAsync(NetworkCredential auth) + { + byte[] methods = auth == null + ? new byte[] { SocksVersion, 0x01, MethodNoAuthentication } + : new byte[] { SocksVersion, 0x01, MethodUsernamePassword }; + + await SendAllAsync(methods).ConfigureAwait(false); + byte[] response = await ReceiveExactAsync(2).ConfigureAwait(false); + + if (response[0] != SocksVersion || response[1] == MethodNoAcceptableMethods) + { + throw ProxyHandshakeFailed(); + } + + switch (response[1]) + { + case MethodNoAuthentication when auth == null: + return; + case MethodUsernamePassword when auth != null: + await AuthenticateUsernamePasswordAsync(auth).ConfigureAwait(false); + return; + default: + throw ProxyHandshakeFailed(); + } + } + + private async Task AuthenticateUsernamePasswordAsync(NetworkCredential auth) + { + byte[] username = Encoding.UTF8.GetBytes(auth.UserName ?? string.Empty); + byte[] password = Encoding.UTF8.GetBytes(auth.Password ?? string.Empty); + if (username.Length is < 1 or > 255 || password.Length is < 1 or > 255) + { + throw new ArgumentException("SOCKS5 username and password must each encode to 1..255 bytes.", nameof(auth)); + } + + var request = new byte[3 + username.Length + password.Length]; + int offset = 0; + request[offset++] = AuthVersion; + request[offset++] = (byte)username.Length; + Buffer.BlockCopy(username, 0, request, offset, username.Length); + offset += username.Length; + request[offset++] = (byte)password.Length; + Buffer.BlockCopy(password, 0, request, offset, password.Length); + + await SendAllAsync(request).ConfigureAwait(false); + byte[] response = await ReceiveExactAsync(2).ConfigureAwait(false); + if (response[0] != AuthVersion || response[1] != 0x00) + { + throw ProxyHandshakeFailed(); + } + } + + private async Task SendConnectRequestAsync(EndPoint destination) + { + byte[] address; + byte addressType; + int port; + + if (destination is DnsEndPoint dnsEndPoint) + { + string asciiHost; + try + { + asciiHost = new IdnMapping().GetAscii(dnsEndPoint.Host); + } + catch (ArgumentException ex) + { + throw new ArgumentException("Invalid SOCKS5 destination host name.", nameof(destination), ex); + } + + address = Encoding.ASCII.GetBytes(asciiHost); + if (address.Length is < 1 or > 255) + { + throw new ArgumentException("SOCKS5 destination host name must encode to 1..255 bytes.", nameof(destination)); + } + + addressType = AddressTypeDomain; + port = dnsEndPoint.Port; + } + else if (destination is IPEndPoint ipEndPoint) + { + address = ipEndPoint.Address.GetAddressBytes(); + addressType = ipEndPoint.AddressFamily switch + { + AddressFamily.InterNetwork => AddressTypeIpv4, + AddressFamily.InterNetworkV6 => AddressTypeIpv6, + _ => throw new ArgumentException(I18N.GetString("Proxy request failed"), nameof(destination)) + }; + port = ipEndPoint.Port; + } + else + { + throw new ArgumentException(I18N.GetString("Proxy request failed"), nameof(destination)); + } + + int addressPrefixLength = addressType == AddressTypeDomain ? 1 : 0; + var request = new byte[4 + addressPrefixLength + address.Length + 2]; + int offset = 0; + request[offset++] = SocksVersion; + request[offset++] = CommandConnect; + request[offset++] = 0x00; + request[offset++] = addressType; + if (addressType == AddressTypeDomain) + { + request[offset++] = (byte)address.Length; + } + + Buffer.BlockCopy(address, 0, request, offset, address.Length); + offset += address.Length; + request[offset++] = (byte)(port >> 8); + request[offset] = (byte)port; + + await SendAllAsync(request).ConfigureAwait(false); + } + + private async Task ReadConnectReplyAsync() + { + byte[] header = await ReceiveExactAsync(4).ConfigureAwait(false); + if (header[0] != SocksVersion || header[1] != 0x00 || header[2] != 0x00) + { + throw ProxyRequestFailed(); + } + + switch (header[3]) + { + case AddressTypeIpv4: + await ReceiveExactAsync(4 + 2).ConfigureAwait(false); + break; + case AddressTypeIpv6: + await ReceiveExactAsync(16 + 2).ConfigureAwait(false); + break; + case AddressTypeDomain: + byte[] length = await ReceiveExactAsync(1).ConfigureAwait(false); + if (length[0] == 0) + { + throw ProxyRequestFailed(); + } + await ReceiveExactAsync(length[0] + 2).ConfigureAwait(false); + break; + default: + throw ProxyRequestFailed(); + } + } + + private async Task SendAllAsync(byte[] buffer) + { + int offset = 0; + while (offset < buffer.Length) + { + int sent = await SendAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false); + if (sent <= 0) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + offset += sent; + } + } + + private Task SendAsync(byte[] buffer, int offset, int count) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + try + { + _remote.BeginSend(buffer, offset, count, SocketFlags.None, ar => + { + try + { + completion.TrySetResult(_remote.EndSend(ar)); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + return completion.Task; + } + + private async Task ReceiveExactAsync(int count) + { + var buffer = new byte[count]; + int offset = 0; + while (offset < count) + { + int received = await ReceiveAsync(buffer, offset, count - offset).ConfigureAwait(false); + if (received <= 0) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + offset += received; + } + return buffer; + } + + private Task ReceiveAsync(byte[] buffer, int offset, int count) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + try + { + _remote.BeginReceive(buffer, offset, count, SocketFlags.None, ar => + { + try + { + completion.TrySetResult(_remote.EndReceive(ar)); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + return completion.Task; + } + + private static InvalidOperationException ProxyHandshakeFailed() + { + return new InvalidOperationException(I18N.GetString("Proxy handshake failed")); + } + + private static InvalidOperationException ProxyRequestFailed() + { + return new InvalidOperationException(I18N.GetString("Proxy request failed")); + } + } +} diff --git a/Shadowsocks.Core/Routing/FilterEngine.cs b/Shadowsocks.Core/Routing/FilterEngine.cs new file mode 100644 index 000000000..514670021 --- /dev/null +++ b/Shadowsocks.Core/Routing/FilterEngine.cs @@ -0,0 +1,937 @@ +#nullable enable + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; + +#if SHADOWSOCKS_NETWORKSERVICE_FILTER_ENGINE +namespace Shadowsocks.NetworkService.ManagedRouting +#else +namespace Shadowsocks.Routing +#endif +{ + /// + /// Network-routing result produced by the managed ABP/EasyList-compatible matcher. + /// The engine intentionally models only the DIRECT/PROXY subset used by Shadowsocks + /// PAC routing; browser cosmetic filtering is outside this contract. + /// + public enum FilterRoutingAction + { + Direct = 0, + Proxy = 1, + } + + public enum FilterRuleSource + { + Fallback = 0, + PrivateNetwork = 1, + User = 2, + Default = 3, + } + + public readonly record struct FilterRoutingDecision( + FilterRoutingAction Action, + FilterRuleSource Source, + string? MatchedRule) + { + public bool IsMatch => Source is FilterRuleSource.User or FilterRuleSource.Default; + + public static FilterRoutingDecision Direct(FilterRuleSource source, string? matchedRule = null) + => new(FilterRoutingAction.Direct, source, matchedRule); + + public static FilterRoutingDecision Proxy(FilterRuleSource source, string matchedRule) + => new(FilterRoutingAction.Proxy, source, matchedRule); + } + + public sealed record InvalidFilterRule(string Text, string Reason); + + public sealed class FilterCompilationReport + { + internal FilterCompilationReport( + int defaultRuleCount, + int userRuleCount, + int optimizedDomainRuleCount, + int unindexedRuleCount, + IReadOnlyList invalidRules) + { + DefaultRuleCount = defaultRuleCount; + UserRuleCount = userRuleCount; + OptimizedDomainRuleCount = optimizedDomainRuleCount; + UnindexedRuleCount = unindexedRuleCount; + InvalidRules = invalidRules; + } + + public int DefaultRuleCount { get; } + public int UserRuleCount { get; } + public int OptimizedDomainRuleCount { get; } + public int UnindexedRuleCount { get; } + public IReadOnlyList InvalidRules { get; } + public int InvalidRuleCount => InvalidRules.Count; + } + + /// + /// Immutable, thread-safe managed matcher for the network-filter subset previously + /// provided by the historical ABP-compatible routing path. User rules are evaluated before generated/default rules; + /// within each rule set, exception rules (@@) win over blocking rules. A blocking rule + /// routes through Shadowsocks, an exception routes DIRECT, and no match falls back DIRECT. + /// + public sealed class FilterEngine + { + private const int MaxCacheEntries = 1024; + private static readonly TimeSpan s_ruleRegexTimeout = TimeSpan.FromMilliseconds(100); + + private static readonly Regex s_optionsRegex = new( + @"\$(?~?[\w-]+(?:=[^,\s]+)?(?:,~?[\w-]+(?:=[^,\s]+)?)*)$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly SearchValues s_invalidSimpleDomainCharacters = SearchValues.Create("/*^|:"); + + private static readonly FrozenSet s_acceptedCompatibilityOptions = new[] + { + "other", "script", "image", "stylesheet", "object", "subdocument", "document", + "xbl", "ping", "xmlhttprequest", "object-subrequest", "dtd", "media", "font", + "background", "popup", "elemhide", "third-party", "collapse", + }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + private readonly RuleMatcher _userMatcher; + private readonly RuleMatcher _defaultMatcher; + private readonly ConcurrentDictionary _resultCache = new(); + + private FilterEngine(RuleMatcher userMatcher, RuleMatcher defaultMatcher) + { + _userMatcher = userMatcher; + _defaultMatcher = defaultMatcher; + } + + public static FilterEngine Compile( + IEnumerable defaultRules, + IEnumerable userRules, + out FilterCompilationReport report) + { + List invalidRules = []; + RuleMatcher defaultMatcher = RuleMatcher.Compile( + defaultRules, "default", invalidRules, out int defaultCount, out int defaultOptimized, out int defaultUnindexed); + RuleMatcher userMatcher = RuleMatcher.Compile( + userRules, "user", invalidRules, out int userCount, out int userOptimized, out int userUnindexed); + report = new FilterCompilationReport( + defaultCount, + userCount, + defaultOptimized + userOptimized, + defaultUnindexed + userUnindexed, + invalidRules.AsReadOnly()); + return new FilterEngine(userMatcher, defaultMatcher); + } + + public static FilterEngine Compile(IEnumerable defaultRules, IEnumerable userRules) + => Compile(defaultRules, userRules, out _); + + /// + /// Reads an EasyList/ABP-style text file for the Shadowsocks user-rule contract: + /// blank lines, comments (!) and section headers ([...]) are ignored, and surrounding + /// whitespace is normalized before a rule is compiled. + /// + public static IReadOnlyList ParseRuleLines(string content) + { + if (string.IsNullOrEmpty(content)) + { + return Array.Empty(); + } + + List rules = []; + using System.IO.StringReader reader = new(content); + for (string? line = reader.ReadLine(); line is not null; line = reader.ReadLine()) + { + string trimmed = line.Trim(); + if (trimmed.Length == 0 || trimmed.StartsWith('!') || trimmed.StartsWith('[')) + { + continue; + } + + rules.Add(trimmed); + } + + return rules; + } + + public FilterRoutingDecision Evaluate(string url, string? host = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(url); + + string normalizedHost = NormalizeHost(url, host); + if (IsLocalBypassIpv4(normalizedHost)) + { + return FilterRoutingDecision.Direct(FilterRuleSource.PrivateNetwork); + } + + CacheKey cacheKey = new(url, normalizedHost); + if (_resultCache.TryGetValue(cacheKey, out FilterRoutingDecision cached)) + { + return cached; + } + + FilterRoutingDecision decision = EvaluateUncached(url, normalizedHost); + if (_resultCache.Count >= MaxCacheEntries) + { + _resultCache.Clear(); + } + _resultCache.TryAdd(cacheKey, decision); + return decision; + } + + private FilterRoutingDecision EvaluateUncached(string url, string host) + { + if (_userMatcher.TryMatch(url, host, out CompiledFilterRule? userRule)) + { + return userRule.IsException + ? FilterRoutingDecision.Direct(FilterRuleSource.User, userRule.Text) + : FilterRoutingDecision.Proxy(FilterRuleSource.User, userRule.Text); + } + + if (_defaultMatcher.TryMatch(url, host, out CompiledFilterRule? defaultRule)) + { + return defaultRule.IsException + ? FilterRoutingDecision.Direct(FilterRuleSource.Default, defaultRule.Text) + : FilterRoutingDecision.Proxy(FilterRuleSource.Default, defaultRule.Text); + } + + return FilterRoutingDecision.Direct(FilterRuleSource.Fallback); + } + + private static string NormalizeHost(string url, string? host) + { + string normalized = host?.Trim().TrimEnd('.') ?? string.Empty; + if (normalized.Length > 0) + { + return normalized; + } + + if (Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) + { + return uri.Host.TrimEnd('.'); + } + + return string.Empty; + } + + private static bool IsLocalBypassIpv4(string host) + { + if (!IPAddress.TryParse(host, out IPAddress? address) + || address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + { + return false; + } + + Span bytes = stackalloc byte[4]; + if (!address.TryWriteBytes(bytes, out int bytesWritten) || bytesWritten != bytes.Length) + { + return false; + } + + return bytes[0] == 10 + || bytes[0] == 127 + || (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) + || (bytes[0] == 192 && bytes[1] == 168); + } + + private static bool IsKeywordCharacter(char character) + => character is >= 'a' and <= 'z' + or >= 'A' and <= 'Z' + or >= '0' and <= '9' + or '%'; + + private static string CreateLowerAsciiKeyword(string text, int start, int length) + => string.Create(length, (text, start), static (destination, state) => + { + for (int i = 0; i < destination.Length; i++) + { + char character = state.text[state.start + i]; + destination[i] = character is >= 'A' and <= 'Z' + ? (char)(character + ('a' - 'A')) + : character; + } + }); + + private readonly record struct CacheKey(string Url, string Host); + + private sealed class RuleMatcher + { + private readonly FrozenDictionary _whitelistDomains; + private readonly FrozenDictionary _blockingDomains; + private readonly FrozenDictionary _whitelistByKeyword; + private readonly FrozenDictionary _blockingByKeyword; + + private RuleMatcher( + FrozenDictionary whitelistDomains, + FrozenDictionary blockingDomains, + FrozenDictionary whitelistByKeyword, + FrozenDictionary blockingByKeyword) + { + _whitelistDomains = whitelistDomains; + _blockingDomains = blockingDomains; + _whitelistByKeyword = whitelistByKeyword; + _blockingByKeyword = blockingByKeyword; + } + + public static RuleMatcher Compile( + IEnumerable? sourceRules, + string sourceName, + List invalidRules, + out int validCount, + out int optimizedDomainCount, + out int unindexedCount) + { + Dictionary> whitelistDomains = new(StringComparer.OrdinalIgnoreCase); + Dictionary> blockingDomains = new(StringComparer.OrdinalIgnoreCase); + Dictionary> whitelist = new(StringComparer.Ordinal); + Dictionary> blocking = new(StringComparer.Ordinal); + HashSet seen = new(StringComparer.Ordinal); + validCount = 0; + optimizedDomainCount = 0; + unindexedCount = 0; + + foreach (string? rawRule in sourceRules ?? Array.Empty()) + { + string? text = rawRule?.Trim(); + if (string.IsNullOrEmpty(text) || text.StartsWith('!') || text.StartsWith('[')) + { + continue; + } + if (!seen.Add(text)) + { + continue; + } + + if (!CompiledFilterRule.TryCreate(text, out CompiledFilterRule? rule, out string? reason)) + { + invalidRules.Add(new InvalidFilterRule(text, $"{sourceName}: {reason ?? "invalid rule"}")); + continue; + } + + if (rule.HostSuffix is string hostSuffix) + { + Dictionary> domainIndex = + rule.IsException ? whitelistDomains : blockingDomains; + if (!domainIndex.TryGetValue(hostSuffix, out List? domainBucket) || domainBucket is null) + { + domainBucket = []; + domainIndex.Add(hostSuffix, domainBucket); + } + domainBucket.Add(rule); + optimizedDomainCount++; + } + else + { + Dictionary> index = rule.IsException ? whitelist : blocking; + if (!index.TryGetValue(rule.Keyword, out List? bucket) || bucket is null) + { + bucket = []; + index.Add(rule.Keyword, bucket); + } + bucket.Add(rule); + if (rule.Keyword.Length == 0) + { + unindexedCount++; + } + } + validCount++; + } + + return new RuleMatcher( + FreezeIndex(whitelistDomains, StringComparer.OrdinalIgnoreCase), + FreezeIndex(blockingDomains, StringComparer.OrdinalIgnoreCase), + FreezeIndex(whitelist, StringComparer.Ordinal), + FreezeIndex(blocking, StringComparer.Ordinal)); + } + + public bool TryMatch(string url, string host, [NotNullWhen(true)] out CompiledFilterRule? matched) + { + // Match CombinedMatcher semantics: any matching exception wins over any + // blocking rule in the same rule set. Simple ||domain^ filters use a + // dedicated suffix index; complex filters use the ABP keyword index. + if (TryMatchDomainIndex(_whitelistDomains, url, host, out matched) + || TryMatchKeywordIndex(_whitelistByKeyword, url, host, out matched)) + { + return true; + } + + return TryMatchDomainIndex(_blockingDomains, url, host, out matched) + || TryMatchKeywordIndex(_blockingByKeyword, url, host, out matched); + } + + private static FrozenDictionary FreezeIndex( + Dictionary> source, + StringComparer comparer) + => source.ToFrozenDictionary( + pair => pair.Key, + pair => pair.Value.ToArray(), + comparer); + + private static bool TryMatchDomainIndex( + FrozenDictionary index, + string url, + string host, + [NotNullWhen(true)] out CompiledFilterRule? matched) + { + string candidate = host.Trim().TrimEnd('.'); + while (candidate.Length > 0) + { + if (index.TryGetValue(candidate, out CompiledFilterRule[]? bucket) && bucket is not null) + { + foreach (CompiledFilterRule rule in bucket) + { + if (rule.Matches(url, host)) + { + matched = rule; + return true; + } + } + } + + int dot = candidate.IndexOf('.'); + if (dot < 0) + { + break; + } + candidate = candidate[(dot + 1)..]; + } + + matched = null; + return false; + } + + private static bool TryMatchKeywordIndex( + FrozenDictionary index, + string url, + string host, + [NotNullWhen(true)] out CompiledFilterRule? matched) + { + return TryMatchKeywordsInText(index, url, url, host, out matched) + || TryMatchKeywordsInText(index, host, url, host, out matched) + || TryMatchKeyword(index, string.Empty, url, host, out matched); + } + + private static bool TryMatchKeywordsInText( + FrozenDictionary index, + string text, + string url, + string host, + [NotNullWhen(true)] out CompiledFilterRule? matched) + { + if (string.IsNullOrEmpty(text)) + { + matched = null; + return false; + } + + int tokenStart = -1; + for (int indexInText = 0; indexInText <= text.Length; indexInText++) + { + bool isKeywordCharacter = indexInText < text.Length && IsKeywordCharacter(text[indexInText]); + if (isKeywordCharacter) + { + tokenStart = tokenStart < 0 ? indexInText : tokenStart; + continue; + } + + if (tokenStart < 0) + { + continue; + } + + int tokenLength = indexInText - tokenStart; + if (tokenLength >= 3) + { + string keyword = CreateLowerAsciiKeyword(text, tokenStart, tokenLength); + if (TryMatchKeyword(index, keyword, url, host, out matched)) + { + return true; + } + } + + tokenStart = -1; + } + + matched = null; + return false; + } + + private static bool TryMatchKeyword( + FrozenDictionary index, + string keyword, + string url, + string host, + [NotNullWhen(true)] out CompiledFilterRule? matched) + { + if (index.TryGetValue(keyword, out CompiledFilterRule[]? bucket) && bucket is not null) + { + foreach (CompiledFilterRule rule in bucket) + { + if (rule.Matches(url, host)) + { + matched = rule; + return true; + } + } + } + + matched = null; + return false; + } + + } + + private sealed class CompiledFilterRule + { + private static readonly string s_separatorPattern = + @"(?:[\x00-\x24\x26-\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]|$)"; + + private readonly Regex? _regex; + private readonly string? _hostSuffix; + private readonly DomainPolicy? _domains; + private readonly bool _requiresSiteKey; + + private CompiledFilterRule( + string text, + bool isException, + Regex? regex, + string? hostSuffix, + DomainPolicy? domains, + string keyword, + bool requiresSiteKey) + { + Text = text; + IsException = isException; + _regex = regex; + _hostSuffix = hostSuffix; + _domains = domains; + Keyword = keyword; + _requiresSiteKey = requiresSiteKey; + } + + public string Text { get; } + public bool IsException { get; } + public string Keyword { get; } + public string? HostSuffix => _hostSuffix; + public bool IsOptimizedDomain => _hostSuffix is not null; + + public bool Matches(string url, string host) + { + if (_requiresSiteKey || !(_domains?.IsActiveOn(host) ?? true)) + { + return false; + } + + if (_hostSuffix is not null) + { + if (host.Equals(_hostSuffix, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + int suffixStart = host.Length - _hostSuffix.Length; + return suffixStart > 0 + && host[suffixStart - 1] == '.' + && host.AsSpan(suffixStart).Equals(_hostSuffix, StringComparison.OrdinalIgnoreCase); + } + + try + { + return _regex?.IsMatch(url) == true; + } + catch (RegexMatchTimeoutException) + { + // One pathological user rule must never stall or break routing. + return false; + } + } + + public static bool TryCreate(string originalText, [NotNullWhen(true)] out CompiledFilterRule? rule, out string? reason) + { + rule = null; + reason = null; + + bool isException = originalText.StartsWith("@@", StringComparison.Ordinal); + string body = isException ? originalText[2..] : originalText; + body = NormalizeShadowsocksDomainRule(body); + + if (!TryParseOptions( + body, + out string patternText, + out string? domainSource, + out bool matchCase, + out bool requiresSiteKey, + out reason)) + { + return false; + } + + if (patternText.Length == 0) + { + reason = "empty filter pattern"; + return false; + } + + try + { + RegexOptions regexOptions = RegexOptions.CultureInvariant; + if (!matchCase) + { + regexOptions |= RegexOptions.IgnoreCase; + } + + string? hostSuffix = null; + Regex? regex = null; + if (!TryGetSimpleDomainAnchor(patternText, matchCase, out hostSuffix)) + { + string regexPattern = IsExplicitRegex(patternText) + ? patternText[1..^1] + : ConvertAbpPatternToRegex(patternText); + + // Complex rules are interpreted rather than RegexOptions.Compiled. + // GeoSite can contain tens of thousands of entries; generating native + // regex code per rule would create unnecessary startup/JIT pressure. + regex = new Regex(regexPattern, regexOptions, s_ruleRegexTimeout); + } + + DomainPolicy? domains = DomainPolicy.Parse(domainSource); + string keyword = FindKeyword(originalText, IsExplicitRegex(patternText)); + rule = new CompiledFilterRule( + originalText, isException, regex, hostSuffix, domains, keyword, requiresSiteKey); + return true; + } + catch (Exception exception) when (exception is ArgumentException or RegexMatchTimeoutException) + { + reason = $"invalid regular expression: {exception.Message}"; + return false; + } + } + + private static bool TryParseOptions( + string body, + out string patternText, + out string? domainSource, + out bool matchCase, + out bool requiresSiteKey, + out string? reason) + { + patternText = body; + domainSource = null; + matchCase = false; + requiresSiteKey = false; + reason = null; + + Match optionsMatch = s_optionsRegex.Match(body); + if (!optionsMatch.Success) + { + return true; + } + + patternText = body[..optionsMatch.Index]; + foreach (string rawOption in optionsMatch.Groups["options"].Value.Split(',')) + { + string option = rawOption; + string? value = null; + int equals = option.IndexOf('='); + if (equals >= 0) + { + value = option[(equals + 1)..]; + option = option[..equals]; + } + + if (option.Equals("match-case", StringComparison.OrdinalIgnoreCase)) + { + matchCase = true; + continue; + } + + if (option.Equals("~match-case", StringComparison.OrdinalIgnoreCase)) + { + matchCase = false; + continue; + } + + if (option.Equals("domain", StringComparison.OrdinalIgnoreCase) && value is not null) + { + domainSource = value; + continue; + } + + if (option.Equals("sitekey", StringComparison.OrdinalIgnoreCase) && value is not null) + { + // Managed routing never supplies an ABP sitekey. Keep the rule valid + // but inactive, matching the network-routing compatibility contract. + requiresSiteKey = true; + continue; + } + + string normalized = option.StartsWith('~') ? option[1..] : option; + if (s_acceptedCompatibilityOptions.Contains(normalized)) + { + // Browser resource/cosmetic options do not alter this network-only + // DIRECT/PROXY decision, but accepting them keeps EasyList compatibility. + continue; + } + + reason = $"unsupported option '{rawOption}'"; + return false; + } + + return true; + } + + private static bool IsExplicitRegex(string pattern) + => pattern.Length >= 2 && pattern[0] == '/' && pattern[^1] == '/'; + + private static string NormalizeShadowsocksDomainRule(string body) + { + Match options = s_optionsRegex.Match(body); + int patternLength = options.Success ? options.Index : body.Length; + string pattern = body[..patternLength]; + if (!pattern.StartsWith("||", StringComparison.Ordinal) + || pattern.EndsWith('^') + || pattern.EndsWith('|')) + { + return body; + } + + // The historical Shadowsocks rule normalization appends a separator to ||domain rules + // before giving them to the ABP matcher. Do it before options here so a + // valid rule such as ||example.com$domain=site.test stays valid. + return pattern + "^" + body[patternLength..]; + } + + private static bool TryGetSimpleDomainAnchor(string pattern, bool matchCase, [NotNullWhen(true)] out string? hostSuffix) + { + hostSuffix = null; + if (matchCase + || !pattern.StartsWith("||", StringComparison.Ordinal) + || !pattern.EndsWith('^')) + { + return false; + } + + string candidate = pattern[2..^1]; + if (candidate.Length == 0 + || candidate.AsSpan().IndexOfAny(s_invalidSimpleDomainCharacters) >= 0 + || ContainsWhitespace(candidate)) + { + return false; + } + + hostSuffix = candidate.TrimEnd('.'); + return hostSuffix.Length > 0; + } + + private static string ConvertAbpPatternToRegex(string pattern) + { + // Preserve the ABP network-filter transformation while avoiding a temporary + // normalized pattern allocation during rule compilation. + if (pattern.EndsWith("^|", StringComparison.Ordinal)) + { + pattern = pattern[..^1]; + } + + bool domainAnchor = pattern.StartsWith("||", StringComparison.Ordinal); + bool startAnchor = !domainAnchor && pattern.StartsWith('|'); + bool endAnchor = pattern.EndsWith('|'); + + int start = domainAnchor ? 2 : startAnchor ? 1 : 0; + int length = pattern.Length - start - (endAnchor ? 1 : 0); + ReadOnlySpan core = length > 0 + ? pattern.AsSpan(start, length) + : ReadOnlySpan.Empty; + + StringBuilder builder = new(); + if (domainAnchor) + { + builder.Append(@"^[\w\-]+://+(?!/)(?:[^/]+\.)?"); + } + else if (startAnchor) + { + builder.Append('^'); + } + + bool previousWildcard = false; + foreach (char character in core) + { + switch (character) + { + case '*': + if (!previousWildcard) + { + builder.Append(".*"); + } + previousWildcard = true; + break; + case '^': + builder.Append(s_separatorPattern); + previousWildcard = false; + break; + default: + builder.Append(Regex.Escape(character.ToString())); + previousWildcard = false; + break; + } + } + + if (endAnchor) + { + builder.Append('$'); + } + + string result = builder.ToString(); + if (!domainAnchor && !startAnchor && result.StartsWith(".*", StringComparison.Ordinal)) + { + result = result[2..]; + } + if (!endAnchor && result.EndsWith(".*", StringComparison.Ordinal)) + { + result = result[..^2]; + } + return result; + } + + private static string FindKeyword(string originalText, bool explicitRegex) + { + if (explicitRegex) + { + return string.Empty; + } + + ReadOnlySpan text = originalText.AsSpan(); + Match options = s_optionsRegex.Match(originalText); + if (options.Success) + { + text = text[..options.Index]; + } + if (text.StartsWith("@@", StringComparison.Ordinal)) + { + text = text[2..]; + } + + int bestStart = -1; + int bestLength = 0; + int tokenStart = -1; + for (int i = 0; i <= text.Length; i++) + { + bool isKeywordCharacter = i < text.Length && IsKeywordCharacter(text[i]); + if (isKeywordCharacter) + { + tokenStart = tokenStart < 0 ? i : tokenStart; + continue; + } + + if (tokenStart < 0) + { + continue; + } + + int tokenLength = i - tokenStart; + bool hasValidLeftBoundary = tokenStart > 0 + && !IsKeywordCharacter(text[tokenStart - 1]) + && text[tokenStart - 1] != '*'; + bool hasValidRightBoundary = i < text.Length + && !IsKeywordCharacter(text[i]) + && text[i] != '*'; + if (tokenLength >= 3 + && hasValidLeftBoundary + && hasValidRightBoundary + && tokenLength > bestLength) + { + bestStart = tokenStart; + bestLength = tokenLength; + } + + tokenStart = -1; + } + + return bestStart >= 0 + ? CreateLowerAsciiKeyword(originalText, bestStart + (originalText.StartsWith("@@", StringComparison.Ordinal) ? 2 : 0), bestLength) + : string.Empty; + } + + private static bool ContainsWhitespace(string value) + { + foreach (char character in value) + { + if (char.IsWhiteSpace(character)) + { + return true; + } + } + + return false; + } + } + + private sealed class DomainPolicy + { + private readonly FrozenDictionary _domains; + private readonly bool _defaultInclude; + + private DomainPolicy(FrozenDictionary domains, bool defaultInclude) + { + _domains = domains; + _defaultInclude = defaultInclude; + } + + public static DomainPolicy? Parse(string? source) + { + if (string.IsNullOrWhiteSpace(source)) + { + return null; + } + + Dictionary domains = new(StringComparer.OrdinalIgnoreCase); + bool hasIncludes = false; + foreach (string raw in source.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + bool include = !raw.StartsWith('~'); + string domain = include ? raw : raw[1..]; + domain = domain.Trim().TrimEnd('.'); + if (domain.Length == 0) + { + continue; + } + hasIncludes |= include; + domains[domain] = include; + } + return new DomainPolicy( + domains.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase), + !hasIncludes); + } + + public bool IsActiveOn(string host) + { + if (string.IsNullOrWhiteSpace(host)) + { + return _defaultInclude; + } + + string candidate = host.Trim().TrimEnd('.'); + while (candidate.Length > 0) + { + if (_domains.TryGetValue(candidate, out bool included)) + { + return included; + } + + int dot = candidate.IndexOf('.'); + if (dot < 0) + { + break; + } + candidate = candidate[(dot + 1)..]; + } + + return _defaultInclude; + } + } + } +} diff --git a/Shadowsocks.Core/Routing/ManagedRoutingSnapshot.cs b/Shadowsocks.Core/Routing/ManagedRoutingSnapshot.cs new file mode 100644 index 000000000..4e8b13c40 --- /dev/null +++ b/Shadowsocks.Core/Routing/ManagedRoutingSnapshot.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Shadowsocks.Routing +{ + /// + /// Describes how the managed network-filter engine participates in routing. + /// Disabled snapshots deliberately preserve the PAC/system-proxy decision. + /// + public enum ManagedRoutingSnapshotMode + { + Disabled = 0, + LocalRules = 1, + ProxyAllBootstrap = 2, + } + + /// + /// Immutable routing snapshot published atomically to live proxy connections. + /// Existing connections keep their already selected route; every new request reads + /// the latest complete snapshot without observing a partially rebuilt rule set. + /// + public sealed class ManagedRoutingSnapshot + { + private static long _nextGeneration; + private long _directDecisionCount; + private long _proxyDecisionCount; + + private ManagedRoutingSnapshot( + ManagedRoutingSnapshotMode mode, + FilterEngine engine, + FilterCompilationReport report, + string source, + DateTime createdUtc, + IReadOnlyList defaultRules, + IReadOnlyList userRules) + { + Mode = mode; + Engine = engine; + Report = report; + Source = source ?? string.Empty; + CreatedUtc = createdUtc; + DefaultRules = defaultRules ?? Array.Empty(); + UserRules = userRules ?? Array.Empty(); + Generation = Interlocked.Increment(ref _nextGeneration); + } + + public long Generation { get; } + public ManagedRoutingSnapshotMode Mode { get; } + public FilterEngine Engine { get; } + public FilterCompilationReport Report { get; } + public string Source { get; } + public DateTime CreatedUtc { get; } + public IReadOnlyList DefaultRules { get; } + public IReadOnlyList UserRules { get; } + public bool IsEnabled => Engine is not null && Mode != ManagedRoutingSnapshotMode.Disabled; + public long DirectDecisionCount => Interlocked.Read(ref _directDecisionCount); + public long ProxyDecisionCount => Interlocked.Read(ref _proxyDecisionCount); + + internal void RecordDecision(FilterRoutingAction action) + { + if (action == FilterRoutingAction.Direct) + { + Interlocked.Increment(ref _directDecisionCount); + } + else + { + Interlocked.Increment(ref _proxyDecisionCount); + } + } + + public static ManagedRoutingSnapshot Disabled(string source) + => new( + ManagedRoutingSnapshotMode.Disabled, + null, + null, + source, + DateTime.UtcNow, + Array.Empty(), + Array.Empty()); + + public static ManagedRoutingSnapshot LocalRules( + FilterEngine engine, + FilterCompilationReport report, + string source = "GeoSite + user rules", + IReadOnlyList defaultRules = null, + IReadOnlyList userRules = null) + { + ArgumentNullException.ThrowIfNull(engine); + ArgumentNullException.ThrowIfNull(report); + return new ManagedRoutingSnapshot( + ManagedRoutingSnapshotMode.LocalRules, + engine, + report, + source, + DateTime.UtcNow, + defaultRules ?? Array.Empty(), + userRules ?? Array.Empty()); + } + + public static ManagedRoutingSnapshot ProxyAllBootstrap( + FilterEngine engine, + FilterCompilationReport report, + string source, + IReadOnlyList defaultRules = null, + IReadOnlyList userRules = null) + { + ArgumentNullException.ThrowIfNull(engine); + ArgumentNullException.ThrowIfNull(report); + return new ManagedRoutingSnapshot( + ManagedRoutingSnapshotMode.ProxyAllBootstrap, + engine, + report, + source, + DateTime.UtcNow, + defaultRules ?? Array.Empty(), + userRules ?? Array.Empty()); + } + } +} diff --git a/Shadowsocks.Core/RuntimeEnvironment.cs b/Shadowsocks.Core/RuntimeEnvironment.cs new file mode 100644 index 000000000..a3ba4e249 --- /dev/null +++ b/Shadowsocks.Core/RuntimeEnvironment.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Shadowsocks.Core +{ + /// + /// Process/runtime information required by the core. The UI initializes this once + /// during startup so core code never needs to depend on Program, WinForms or WPF. + /// + public static class RuntimeEnvironment + { + private static string[] _arguments = Array.Empty(); + + public static string ExecutablePath { get; private set; } = + Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? AppContext.BaseDirectory; + + public static string WorkingDirectory { get; private set; } = + Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? AppContext.BaseDirectory; + + public static IReadOnlyList Arguments => _arguments; + + public static void Initialize(string executablePath, string workingDirectory, IEnumerable arguments) + { + if (!string.IsNullOrWhiteSpace(executablePath)) + ExecutablePath = Path.GetFullPath(executablePath); + if (!string.IsNullOrWhiteSpace(workingDirectory)) + WorkingDirectory = Path.GetFullPath(workingDirectory); + _arguments = arguments?.ToArray() ?? Array.Empty(); + } + } +} diff --git a/Shadowsocks.Core/Settings/AppSettings.cs b/Shadowsocks.Core/Settings/AppSettings.cs new file mode 100644 index 000000000..808dc6033 --- /dev/null +++ b/Shadowsocks.Core/Settings/AppSettings.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.Json; + +namespace Shadowsocks.Core.Settings; + +public static class AppSettings +{ + private static readonly JsonSerializerOptions s_jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + private static readonly Lazy s_settings = new(Load); + + public static LoggingSettings Logging => s_settings.Value.Logging; + + private static AppSettingsDocument Load() + { + try + { + string json = EmbeddedResources.AppSettingsJson; + return JsonSerializer.Deserialize(json, s_jsonOptions) ?? new AppSettingsDocument(); + } + catch (Exception exception) + { + throw new InvalidOperationException("The embedded appsettings.json is invalid.", exception); + } + } +} + +public sealed class LoggingSettings +{ + public string FilePath { get; set; } = "shadowsocks.log"; + + public string FallbackFilePath { get; set; } = "shadowsocks.log"; + + public string MinimumLevel { get; set; } = "Info"; + + public string VerboseMinimumLevel { get; set; } = "Debug"; + + public long ArchiveAboveSizeBytes { get; set; } = 10L * 1024 * 1024; + + public int MaxArchiveFiles { get; set; } = 7; + + public int MaxArchiveDays { get; set; } = 7; + + public string Layout { get; set; } = + "${longdate}|${level:uppercase=true}|${logger}|${message}${onexception:inner=${newline}${exception:format=tostring}}"; +} + +public sealed class AppSettingsDocument +{ + public LoggingSettings Logging { get; set; } = new(); +} diff --git a/Shadowsocks.Core/Shadowsocks.Core.csproj b/Shadowsocks.Core/Shadowsocks.Core.csproj new file mode 100644 index 000000000..97b1dc8e6 --- /dev/null +++ b/Shadowsocks.Core/Shadowsocks.Core.csproj @@ -0,0 +1,30 @@ + + + net10.0 + Shadowsocks community + https://github.com/SlimRG/shadowsocks-reborn + git + Shadowsocks + Shadowsocks.Core + x64 + AnyCPU + disable + disable + + + + + + + + + + + + + + + + + + diff --git a/Shadowsocks.Core/Storage/AppStoragePaths.cs b/Shadowsocks.Core/Storage/AppStoragePaths.cs new file mode 100644 index 000000000..1fcb2a0b2 --- /dev/null +++ b/Shadowsocks.Core/Storage/AppStoragePaths.cs @@ -0,0 +1,194 @@ +using System; +using System.IO; + +namespace Shadowsocks.Core.Storage +{ + public static class AppStoragePaths + { + public const string ProductDirectoryName = "Shadowsocks"; + private const string CleanDirectoryName = "Clean"; + private static readonly object SyncRoot = new(); + private static bool initialized; + private static bool cleanMode; + private static string storageRoot; + private static CleanStorageSession cleanSession; + + public static string LocalAppDataRoot => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ProductDirectoryName); + + public static string StorageRoot + { + get + { + EnsureInitialized(); + return storageRoot; + } + } + + public static bool IsCleanMode + { + get + { + EnsureInitialized(); + return cleanMode; + } + } + + public static string SettingsFile => Path.Combine(StorageRoot, "settings.json"); + public static string SettingsBackupFile => Path.Combine(StorageRoot, "settings.backup.json"); + + public static string CacheRoot => Path.Combine(StorageRoot, "Cache"); + public static string PacCacheDirectory => Path.Combine(CacheRoot, "PAC"); + public static string OnlinePacCacheFile => Path.Combine(PacCacheDirectory, "online-pac-cache.pac"); + public static string OnlinePacMetadataFile => Path.Combine(PacCacheDirectory, "online-pac-cache.meta.json"); + public static string GeositeCacheDirectory => Path.Combine(CacheRoot, "GeoSite"); + + public static string DataRoot => Path.Combine(StorageRoot, "Data"); + public static string PacDataDirectory => Path.Combine(DataRoot, "PAC"); + public static string LocalPacFile => Path.Combine(PacDataDirectory, "pac.txt"); + public static string UserRuleFile => Path.Combine(PacDataDirectory, "user-rule.txt"); + + public static string PluginsDirectory => Path.Combine(StorageRoot, "Plugins"); + public static string ComponentsDirectory => Path.Combine(StorageRoot, "Components"); + public static string DnsCryptComponentDirectory => Path.Combine(ComponentsDirectory, "DNSCryptProxy"); + + public static string LogsDirectory => Path.Combine(StorageRoot, "Logs"); + public static string LogFile => Path.Combine(LogsDirectory, "shadowsocks.log"); + public static string RuntimeRoot => Path.Combine(StorageRoot, "Runtime"); + public static string WinDivertRuntimeRoot => Path.Combine(RuntimeRoot, "WinDivert"); + public static string DnsCryptRuntimeDirectory => Path.Combine(RuntimeRoot, "DNSCryptProxy"); + public static string StartupDirectory => Path.Combine(StorageRoot, "Startup"); + public static string StartupExecutableFile => Path.Combine(StartupDirectory, "Shadowsocks.exe"); + + public static string SystemTempProductRoot => Path.Combine(Path.GetTempPath(), ProductDirectoryName); + public static string CleanSessionsRoot => Path.Combine(SystemTempProductRoot, CleanDirectoryName); + public static string TempRoot => Path.Combine(StorageRoot, "Temp"); + public static string TempNetworkServiceRoot => Path.Combine(TempRoot, "NetworkService"); + public static string TempUpdatesRoot => Path.Combine(TempRoot, "Updates"); + public static string DnsCryptUpdateDirectory => Path.Combine(TempUpdatesRoot, "DNSCryptProxy"); + public static string TempWorkingRoot => Path.Combine(TempRoot, "Working"); + public static string TempStartupLogsRoot => Path.Combine(TempRoot, "StartupLogs"); + + public static void Initialize(string executablePath) + { + lock (SyncRoot) + { + if (initialized) + return; + + CleanupStaleCleanSessions(TimeSpan.FromDays(2)); + + cleanMode = IsCleanModeExecutableName(executablePath); + if (cleanMode) + { + cleanSession = CleanStorageSession.Create( + CleanSessionsRoot, + DateTime.UtcNow, + Environment.ProcessId, + Guid.NewGuid()); + storageRoot = cleanSession.Root; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + else + { + storageRoot = LocalAppDataRoot; + } + + initialized = true; + } + } + + public static bool IsCleanModeExecutableName(string executablePath) + { + if (string.IsNullOrWhiteSpace(executablePath)) + return false; + + string fileName = Path.GetFileNameWithoutExtension(executablePath.Trim()); + return !string.IsNullOrWhiteSpace(fileName) + && fileName.EndsWith("p", StringComparison.OrdinalIgnoreCase); + } + + public static void EnsureStorageDirectories() + { + Directory.CreateDirectory(StorageRoot); + Directory.CreateDirectory(CacheRoot); + Directory.CreateDirectory(PacCacheDirectory); + Directory.CreateDirectory(GeositeCacheDirectory); + Directory.CreateDirectory(DataRoot); + Directory.CreateDirectory(PacDataDirectory); + Directory.CreateDirectory(PluginsDirectory); + Directory.CreateDirectory(ComponentsDirectory); + Directory.CreateDirectory(LogsDirectory); + Directory.CreateDirectory(RuntimeRoot); + Directory.CreateDirectory(TempRoot); + + if (!IsCleanMode) + { + Directory.CreateDirectory(StartupDirectory); + } + } + + public static string EnsureTempDirectory(string path) + { + Directory.CreateDirectory(path); + return path; + } + + public static void CleanupCleanSession() + { + CleanStorageSession session; + + lock (SyncRoot) + { + if (!initialized || !cleanMode || cleanSession is null) + return; + + session = cleanSession; + cleanSession = null; + } + + session.Dispose(); + } + + public static void CleanupStaleCleanSessions(TimeSpan retention) + { + string root = CleanSessionsRoot; + if (!Directory.Exists(root)) + return; + + DateTime cutoffUtc = DateTime.UtcNow - retention; + foreach (string directory in Directory.EnumerateDirectories(root)) + { + try + { + if (Directory.GetLastWriteTimeUtc(directory) > cutoffUtc || IsCleanSessionActive(directory)) + continue; + + Directory.Delete(directory, recursive: true); + } + catch + { + } + } + } + + private static void EnsureInitialized() + { + if (initialized) + return; + + Initialize(Environment.ProcessPath ?? AppContext.BaseDirectory); + } + + private static bool IsCleanSessionActive(string directory) + { + return CleanStorageSession.IsActive(directory); + } + + private static void OnProcessExit(object sender, EventArgs e) + { + CleanupCleanSession(); + } + } +} diff --git a/Shadowsocks.Core/Storage/CleanStorageSession.cs b/Shadowsocks.Core/Storage/CleanStorageSession.cs new file mode 100644 index 000000000..30751b79b --- /dev/null +++ b/Shadowsocks.Core/Storage/CleanStorageSession.cs @@ -0,0 +1,129 @@ +#nullable enable +using System; +using System.IO; +using System.Threading; + +namespace Shadowsocks.Core.Storage +{ + /// + /// Owns one Clean Mode storage session and its exclusive lifetime lock. + /// Disposing the session releases the lock before removing the entire session tree. + /// + internal sealed class CleanStorageSession : IDisposable + { + internal const string LockFileName = ".session.lock"; + + private FileStream? sessionLock; + private int disposed; + + private CleanStorageSession(string root, FileStream sessionLock) + { + Root = root; + this.sessionLock = sessionLock; + } + + public string Root { get; } + + public static CleanStorageSession Create( + string sessionsRoot, + DateTime utcNow, + int processId, + Guid sessionId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(sessionsRoot); + string sessionName = $"{utcNow:yyyyMMddHHmmss}-{processId}-{sessionId:N}"; + string root = Path.Combine(sessionsRoot, sessionName); + Directory.CreateDirectory(root); + + try + { + string lockPath = Path.Combine(root, LockFileName); + FileStream sessionLock = new( + lockPath, + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.Read, + bufferSize: 1, + FileOptions.DeleteOnClose); + return new CleanStorageSession(root, sessionLock); + } + catch + { + TryDeleteDirectory(root); + throw; + } + } + + public void Dispose() + { + if (Interlocked.Exchange(ref disposed, 1) != 0) + return; + + FileStream? ownedLock = Interlocked.Exchange(ref sessionLock, null); + try + { + ownedLock?.Dispose(); + } + catch + { + } + + TryDeleteDirectory(Root); + } + + internal static bool IsActive(string directory) + { + string lockPath = Path.Combine(directory, LockFileName); + if (!File.Exists(lockPath)) + return false; + + try + { + using FileStream _ = new( + lockPath, + FileMode.Open, + FileAccess.ReadWrite, + FileShare.None, + bufferSize: 1, + FileOptions.None); + return false; + } + catch (IOException) + { + return true; + } + catch (UnauthorizedAccessException) + { + return true; + } + } + + internal static void TryDeleteDirectory(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + for (int attempt = 0; attempt < 3; attempt++) + { + try + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + return; + } + catch (IOException) when (attempt < 2) + { + Thread.Sleep(50); + } + catch (UnauthorizedAccessException) when (attempt < 2) + { + Thread.Sleep(50); + } + catch + { + return; + } + } + } + } +} diff --git a/Shadowsocks.Core/Storage/ISettingsStore.cs b/Shadowsocks.Core/Storage/ISettingsStore.cs new file mode 100644 index 000000000..a67fcd622 --- /dev/null +++ b/Shadowsocks.Core/Storage/ISettingsStore.cs @@ -0,0 +1,13 @@ +using System; + +namespace Shadowsocks.Core.Storage +{ + public interface ISettingsStore + { + bool TryGetString(string name, out string value); + void SetString(string name, string value); + bool TryGetInt32(string name, out int value); + void SetInt32(string name, int value); + void DeleteValue(string name); + } +} diff --git a/Shadowsocks.Core/Storage/JsonFileSettingsStore.cs b/Shadowsocks.Core/Storage/JsonFileSettingsStore.cs new file mode 100644 index 000000000..c089b3f61 --- /dev/null +++ b/Shadowsocks.Core/Storage/JsonFileSettingsStore.cs @@ -0,0 +1,223 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Shadowsocks.Core.Storage +{ + public sealed class JsonFileSettingsStore : ISettingsStore + { + private readonly object syncRoot = new(); + private readonly string filePath; + private readonly string backupPath; + + public JsonFileSettingsStore(string filePath, string backupPath) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("Settings file path must not be empty.", nameof(filePath)); + if (string.IsNullOrWhiteSpace(backupPath)) + throw new ArgumentException("Settings backup path must not be empty.", nameof(backupPath)); + + this.filePath = Path.GetFullPath(filePath); + this.backupPath = Path.GetFullPath(backupPath); + } + + public bool TryGetString(string name, out string value) + { + lock (syncRoot) + { + JObject document = LoadDocument(); + if (!document.TryGetValue(name, StringComparison.Ordinal, out JToken token) + || token is null + || token.Type == JTokenType.Null) + { + value = null; + return false; + } + + value = token.Type == JTokenType.String + ? token.Value() + : token.ToString(Formatting.Indented); + return value is not null; + } + } + + public void SetString(string name, string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(value); + + lock (syncRoot) + { + JObject document = LoadDocument(); + document[name] = ParseJsonValue(value); + SaveDocument(document); + } + } + + public bool TryGetInt32(string name, out int value) + { + lock (syncRoot) + { + JObject document = LoadDocument(); + if (document.TryGetValue(name, StringComparison.Ordinal, out JToken token) + && token is not null + && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + + value = default; + return false; + } + } + + public void SetInt32(string name, int value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + lock (syncRoot) + { + JObject document = LoadDocument(); + document[name] = value; + SaveDocument(document); + } + } + + public void DeleteValue(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return; + + lock (syncRoot) + { + JObject document = LoadDocument(); + if (document.Remove(name)) + { + SaveDocument(document); + } + } + } + + private JObject LoadDocument() + { + if (TryLoadDocument(filePath, out JObject document)) + return document; + + if (TryLoadDocument(backupPath, out document)) + { + TryRestoreBackup(document); + return document; + } + + return new JObject(); + } + + private void SaveDocument(JObject document) + { + string directory = Path.GetDirectoryName(filePath) + ?? throw new InvalidOperationException("Settings path has no parent directory."); + Directory.CreateDirectory(directory); + + string temporaryPath = Path.Combine( + directory, + $"{Path.GetFileName(filePath)}.tmp.{Environment.ProcessId}.{Guid.NewGuid():N}"); + + try + { + File.WriteAllText(temporaryPath, document.ToString(Formatting.Indented)); + + if (File.Exists(filePath)) + { + File.Copy(filePath, backupPath, overwrite: true); + File.Replace(temporaryPath, filePath, destinationBackupFileName: null, ignoreMetadataErrors: true); + } + else + { + File.Move(temporaryPath, filePath); + } + } + finally + { + TryDelete(temporaryPath); + } + } + + private void TryRestoreBackup(JObject document) + { + try + { + string directory = Path.GetDirectoryName(filePath) + ?? throw new InvalidOperationException("Settings path has no parent directory."); + Directory.CreateDirectory(directory); + string temporaryPath = Path.Combine( + directory, + $"{Path.GetFileName(filePath)}.restore.{Environment.ProcessId}.{Guid.NewGuid():N}"); + try + { + File.WriteAllText(temporaryPath, document.ToString(Formatting.Indented)); + File.Move(temporaryPath, filePath, overwrite: true); + } + finally + { + TryDelete(temporaryPath); + } + } + catch + { + // The caller can still use the valid backup in memory even if the primary + // file cannot be repaired at this moment. + } + } + + private static bool TryLoadDocument(string path, out JObject document) + { + document = null; + if (!File.Exists(path)) + return false; + + try + { + string json = File.ReadAllText(path); + document = JObject.Parse(json); + return true; + } + catch (JsonException) + { + return false; + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + } + + private static JToken ParseJsonValue(string value) + { + try + { + return JToken.Parse(value); + } + catch (JsonException) + { + return new JValue(value); + } + } + + private static void TryDelete(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch + { + } + } + } +} diff --git a/shadowsocks-csharp/Util/Sockets/SocketUtil.cs b/Shadowsocks.Core/Util/Sockets/SocketUtil.cs similarity index 99% rename from shadowsocks-csharp/Util/Sockets/SocketUtil.cs rename to Shadowsocks.Core/Util/Sockets/SocketUtil.cs index e80356760..08e0fbc28 100644 --- a/shadowsocks-csharp/Util/Sockets/SocketUtil.cs +++ b/Shadowsocks.Core/Util/Sockets/SocketUtil.cs @@ -60,6 +60,6 @@ public static void FullClose(this System.Net.Sockets.Socket s) { } } - + } } diff --git a/shadowsocks-csharp/Util/Sockets/WrappedSocket.cs b/Shadowsocks.Core/Util/Sockets/WrappedSocket.cs similarity index 76% rename from shadowsocks-csharp/Util/Sockets/WrappedSocket.cs rename to Shadowsocks.Core/Util/Sockets/WrappedSocket.cs index c5308c903..2e3536581 100644 --- a/shadowsocks-csharp/Util/Sockets/WrappedSocket.cs +++ b/Shadowsocks.Core/Util/Sockets/WrappedSocket.cs @@ -1,268 +1,245 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace Shadowsocks.Util.Sockets -{ - /* - * A wrapped socket class which support both ipv4 and ipv6 based on the - * connected remote endpoint. - * - * If the server address is host name, then it may have both ipv4 and ipv6 address - * after resolving. The main idea is we don't want to resolve and choose the address - * by ourself. Instead, Socket.ConnectAsync() do handle this thing internally by trying - * each address and returning an established socket connection. - */ - public class WrappedSocket - { - public EndPoint LocalEndPoint => _activeSocket?.LocalEndPoint; - - // Only used during connection and close, so it won't cost too much. - private SpinLock _socketSyncLock = new SpinLock(); - - private bool _disposed; - private bool Connected => _activeSocket != null; - private Socket _activeSocket; - - - public void BeginConnect(EndPoint remoteEP, AsyncCallback callback, object state) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (Connected) - { - throw new SocketException((int) SocketError.IsConnected); - } - - var arg = new SocketAsyncEventArgs(); - arg.RemoteEndPoint = remoteEP; - arg.Completed += OnTcpConnectCompleted; - arg.UserToken = new TcpUserToken(callback, state); - - if(!Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, arg)) - { - OnTcpConnectCompleted(this, arg); - } - } - - private class FakeAsyncResult : IAsyncResult - { - public bool IsCompleted { get; } = true; - public WaitHandle AsyncWaitHandle { get; } = null; - public object AsyncState { get; set; } - public bool CompletedSynchronously { get; } = true; - public Exception InternalException { get; set; } = null; - } - - private class TcpUserToken - { - public AsyncCallback Callback { get; } - public object AsyncState { get; } - - public TcpUserToken(AsyncCallback callback, object state) - { - Callback = callback; - AsyncState = state; - } - } - - private void OnTcpConnectCompleted(object sender, SocketAsyncEventArgs args) - { - using (args) - { - args.Completed -= OnTcpConnectCompleted; - var token = (TcpUserToken) args.UserToken; - - if (args.SocketError != SocketError.Success) - { - var ex = args.ConnectByNameError ?? new SocketException((int) args.SocketError); - - var r = new FakeAsyncResult() - { - AsyncState = token.AsyncState, - InternalException = ex - }; - - token.Callback(r); - } - else - { - var lockTaken = false; - if (!_socketSyncLock.IsHeldByCurrentThread) - { - _socketSyncLock.TryEnter(ref lockTaken); - } - try - { - if (Connected) - { - args.ConnectSocket.FullClose(); - } - else - { - _activeSocket = args.ConnectSocket; - if (_disposed) - { - _activeSocket.FullClose(); - } - - var r = new FakeAsyncResult() - { - AsyncState = token.AsyncState - }; - token.Callback(r); - } - } - finally - { - if (lockTaken) - { - _socketSyncLock.Exit(); - } - } - } - } - } - - public void EndConnect(IAsyncResult asyncResult) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - - var r = asyncResult as FakeAsyncResult; - if (r == null) - { - throw new ArgumentException("Invalid asyncResult.", nameof(asyncResult)); - } - - if (r.InternalException != null) - { - throw r.InternalException; - } - } - - public void Dispose() - { - if (_disposed) - { - return; - } - var lockTaken = false; - if (!_socketSyncLock.IsHeldByCurrentThread) - { - _socketSyncLock.TryEnter(ref lockTaken); - } - try - { - _disposed = true; - _activeSocket?.FullClose(); - } - finally - { - if (lockTaken) - { - _socketSyncLock.Exit(); - } - } - - } - - public IAsyncResult BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, - AsyncCallback callback, - object state) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - throw new SocketException((int) SocketError.NotConnected); - } - - return _activeSocket.BeginSend(buffer, offset, size, socketFlags, callback, state); - } - - public int EndSend(IAsyncResult asyncResult) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - throw new SocketException((int) SocketError.NotConnected); - } - - return _activeSocket.EndSend(asyncResult); - } - - public IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, - AsyncCallback callback, - object state) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - throw new SocketException((int) SocketError.NotConnected); - } - - return _activeSocket.BeginReceive(buffer, offset, size, socketFlags, callback, state); - } - - public int EndReceive(IAsyncResult asyncResult) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - throw new SocketException((int) SocketError.NotConnected); - } - - return _activeSocket.EndReceive(asyncResult); - } - - public void Shutdown(SocketShutdown how) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - return; - } - - _activeSocket.Shutdown(how); - } - - public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, bool optionValue) - { - SetSocketOption(optionLevel, optionName, optionValue ? 1 : 0); - } - - public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - if (!Connected) - { - throw new SocketException((int)SocketError.NotConnected); - } - - _activeSocket.SetSocketOption(optionLevel, optionName, optionValue); - } - } -} +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Shadowsocks.Util.Sockets +{ + /* + * A wrapped socket class which support both ipv4 and ipv6 based on the + * connected remote endpoint. + * + * If the server address is host name, then it may have both ipv4 and ipv6 address + * after resolving. The main idea is we don't want to resolve and choose the address + * by ourself. Instead, Socket.ConnectAsync() do handle this thing internally by trying + * each address and returning an established socket connection. + */ + public class WrappedSocket : IDisposable + { + public EndPoint LocalEndPoint => _activeSocket?.LocalEndPoint; + + // Only used during connection and close, so it won't cost too much. + private SpinLock _socketSyncLock; + + private bool _disposed; + private bool Connected => _activeSocket != null; + private Socket _activeSocket; + + + public void BeginConnect(EndPoint remoteEP, AsyncCallback callback, object state) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (Connected) + { + throw new SocketException((int)SocketError.IsConnected); + } + + var arg = new SocketAsyncEventArgs(); + arg.RemoteEndPoint = remoteEP; + arg.Completed += OnTcpConnectCompleted; + arg.UserToken = new TcpUserToken(callback, state); + + if (!Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, arg)) + { + OnTcpConnectCompleted(this, arg); + } + } + + private class FakeAsyncResult : IAsyncResult + { + public bool IsCompleted { get; } = true; + public WaitHandle AsyncWaitHandle => null; + public object AsyncState { get; set; } + public bool CompletedSynchronously { get; } = true; + public Exception InternalException { get; set; } + } + + private class TcpUserToken + { + public AsyncCallback Callback { get; } + public object AsyncState { get; } + + public TcpUserToken(AsyncCallback callback, object state) + { + Callback = callback; + AsyncState = state; + } + } + + private void OnTcpConnectCompleted(object sender, SocketAsyncEventArgs args) + { + using (args) + { + args.Completed -= OnTcpConnectCompleted; + var token = (TcpUserToken)args.UserToken; + + if (args.SocketError != SocketError.Success) + { + var ex = args.ConnectByNameError ?? new SocketException((int)args.SocketError); + + var r = new FakeAsyncResult() + { + AsyncState = token.AsyncState, + InternalException = ex + }; + + token.Callback(r); + } + else + { + var lockTaken = false; + if (!_socketSyncLock.IsHeldByCurrentThread) + { + _socketSyncLock.TryEnter(ref lockTaken); + } + try + { + if (Connected) + { + args.ConnectSocket.FullClose(); + } + else + { + _activeSocket = args.ConnectSocket; + if (_disposed) + { + _activeSocket.FullClose(); + } + + var r = new FakeAsyncResult() + { + AsyncState = token.AsyncState + }; + token.Callback(r); + } + } + finally + { + if (lockTaken) + { + _socketSyncLock.Exit(); + } + } + } + } + } + + public void EndConnect(IAsyncResult asyncResult) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var r = asyncResult as FakeAsyncResult; + if (r == null) + { + throw new ArgumentException("Invalid asyncResult.", nameof(asyncResult)); + } + + if (r.InternalException != null) + { + throw r.InternalException; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + var lockTaken = false; + if (!_socketSyncLock.IsHeldByCurrentThread) + { + _socketSyncLock.TryEnter(ref lockTaken); + } + try + { + _disposed = true; + _activeSocket?.FullClose(); + } + finally + { + if (lockTaken) + { + _socketSyncLock.Exit(); + } + } + + GC.SuppressFinalize(this); + } + + public IAsyncResult BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, + AsyncCallback callback, + object state) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + throw new SocketException((int)SocketError.NotConnected); + } + + return _activeSocket.BeginSend(buffer, offset, size, socketFlags, callback, state); + } + + public int EndSend(IAsyncResult asyncResult) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + throw new SocketException((int)SocketError.NotConnected); + } + + return _activeSocket.EndSend(asyncResult); + } + + public IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, + AsyncCallback callback, + object state) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + throw new SocketException((int)SocketError.NotConnected); + } + + return _activeSocket.BeginReceive(buffer, offset, size, socketFlags, callback, state); + } + + public int EndReceive(IAsyncResult asyncResult) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + throw new SocketException((int)SocketError.NotConnected); + } + + return _activeSocket.EndReceive(asyncResult); + } + + public void Shutdown(SocketShutdown how) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + return; + } + + _activeSocket.Shutdown(how); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, bool optionValue) + { + SetSocketOption(optionLevel, optionName, optionValue ? 1 : 0); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!Connected) + { + throw new SocketException((int)SocketError.NotConnected); + } + + _activeSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + } +} diff --git a/Shadowsocks.NetworkService/BrokerHost.cs b/Shadowsocks.NetworkService/BrokerHost.cs new file mode 100644 index 000000000..0f1fea886 --- /dev/null +++ b/Shadowsocks.NetworkService/BrokerHost.cs @@ -0,0 +1,120 @@ +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService; + +internal static class BrokerHost +{ + public static async Task RunAsync(string pipeName, string? startupLogPath) + { + await using var supervisor = new CaptureChildSupervisor(); + try + { + using NamedPipeClientStream pipe = new( + ".", + pipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous); + using CancellationTokenSource connectTimeout = new(TimeSpan.FromSeconds(30)); + await pipe.ConnectAsync(connectTimeout.Token).ConfigureAwait(false); + + using StreamReader reader = new(pipe, Encoding.UTF8, false, 4096, leaveOpen: true); + using StreamWriter writer = new(pipe, new UTF8Encoding(false), 4096, leaveOpen: true) + { + AutoFlush = true, + }; + + while (true) + { + string? line = await reader.ReadLineAsync().ConfigureAwait(false); + if (line is null) + { + break; + } + + ControlRequest? control; + try + { + control = JsonSerializer.Deserialize(line, NetworkServiceJsonContext.Default.ControlRequest); + } + catch (Exception exception) + { + await NetworkServiceResponses.SendAsync( + writer, + NetworkServiceResponses.Failure("Invalid control request: " + exception.Message)).ConfigureAwait(false); + continue; + } + + string command = control?.Command ?? string.Empty; + ServiceResponse response; + if (command.Equals("start", StringComparison.OrdinalIgnoreCase) + || command.Equals("restart", StringComparison.OrdinalIgnoreCase)) + { + StartRequest? request = JsonSerializer.Deserialize(line, NetworkServiceJsonContext.Default.StartRequest); + response = request is null + ? NetworkServiceResponses.Failure("Invalid capture configuration.") + : await supervisor.StartOrRestartAsync(request).ConfigureAwait(false); + } + else if (command.Equals("game-on", StringComparison.OrdinalIgnoreCase)) + { + response = await supervisor.EnterGameModeAsync().ConfigureAwait(false); + } + else if (command.Equals("game-off", StringComparison.OrdinalIgnoreCase)) + { + response = await supervisor.ExitGameModeAsync().ConfigureAwait(false); + } + else if (command.Equals("ping", StringComparison.OrdinalIgnoreCase)) + { + response = await supervisor.PingAsync().ConfigureAwait(false); + } + else if (command.Equals("stop", StringComparison.OrdinalIgnoreCase)) + { + response = await supervisor.StopAsync().ConfigureAwait(false); + await NetworkServiceResponses.SendAsync(writer, response).ConfigureAwait(false); + break; + } + else + { + response = NetworkServiceResponses.Failure("Unknown command."); + } + + await NetworkServiceResponses.SendAsync(writer, response).ConfigureAwait(false); + } + + return 0; + } + catch (OperationCanceledException exception) + { + TryWriteStartupFailure(startupLogPath, "NetworkService broker startup timed out or was cancelled: " + exception); + return 4; + } + catch (Exception exception) + { + TryWriteStartupFailure(startupLogPath, "NetworkService broker failed before the control channel was established: " + exception); + return 1; + } + } + + private static void TryWriteStartupFailure(string? path, string message) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + string? directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + File.WriteAllText(path, message, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + } + catch + { + } + } +} diff --git a/Shadowsocks.NetworkService/CaptureChild.cs b/Shadowsocks.NetworkService/CaptureChild.cs new file mode 100644 index 000000000..bcb588233 --- /dev/null +++ b/Shadowsocks.NetworkService/CaptureChild.cs @@ -0,0 +1,195 @@ +using System.Diagnostics; +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService; + +internal sealed class CaptureChild : IAsyncDisposable +{ + private readonly Process _process; + private readonly NamedPipeServerStream _pipe; + private readonly StreamReader _reader; + private readonly StreamWriter _writer; + private bool _disposed; + + private CaptureChild( + Process process, + NamedPipeServerStream pipe, + StreamReader reader, + StreamWriter writer, + int tcpRedirectPort, + int udpRedirectPort, + bool dnsInterceptionActive, + bool dnsFailClosedActive, + bool managedRoutingActive, + int managedRoutingRuleCount) + { + _process = process; + _pipe = pipe; + _reader = reader; + _writer = writer; + TcpRedirectPort = tcpRedirectPort; + UdpRedirectPort = udpRedirectPort; + DnsInterceptionActive = dnsInterceptionActive; + DnsFailClosedActive = dnsFailClosedActive; + ManagedRoutingActive = managedRoutingActive; + ManagedRoutingRuleCount = managedRoutingRuleCount; + StartedUtc = DateTime.UtcNow; + } + + public int TcpRedirectPort { get; } + public int UdpRedirectPort { get; } + public DateTime StartedUtc { get; } + public bool DnsInterceptionActive { get; } + public bool DnsFailClosedActive { get; } + public bool ManagedRoutingActive { get; } + public int ManagedRoutingRuleCount { get; } + + public bool IsAlive + { + get + { + try + { + return !_disposed && !_process.HasExited && _pipe.IsConnected; + } + catch + { + return false; + } + } + } + + public static async Task StartAsync(StartRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + string pipeName = "Shadowsocks.Capture." + Guid.NewGuid().ToString("N"); + NamedPipeServerStream pipe = new( + pipeName, + PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.WriteThrough); + Process? process = null; + StreamReader? reader = null; + StreamWriter? writer = null; + + try + { + string executable = Environment.ProcessPath + ?? throw new InvalidOperationException("Unable to locate network service executable."); + var startInfo = new ProcessStartInfo + { + FileName = executable, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = AppContext.BaseDirectory, + }; + startInfo.ArgumentList.Add("--capture-pipe"); + startInfo.ArgumentList.Add(pipeName); + process = Process.Start(startInfo) + ?? throw new InvalidOperationException("Unable to start WinDivert capture child."); + + using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(15)); + await pipe.WaitForConnectionAsync(timeout.Token).ConfigureAwait(false); + reader = new StreamReader(pipe, Encoding.UTF8, false, 4096, leaveOpen: true); + writer = new StreamWriter(pipe, new UTF8Encoding(false), 4096, leaveOpen: true) + { + AutoFlush = true, + }; + + request.Command = "start"; + await writer.WriteLineAsync( + JsonSerializer.Serialize(request, NetworkServiceJsonContext.Default.StartRequest)).ConfigureAwait(false); + string? line = await reader.ReadLineAsync(timeout.Token).ConfigureAwait(false); + ServiceResponse? response = string.IsNullOrWhiteSpace(line) + ? null + : JsonSerializer.Deserialize(line, NetworkServiceJsonContext.Default.ServiceResponse); + if (response?.Success != true) + { + throw new InvalidOperationException(response?.Message ?? "WinDivert capture child failed to start."); + } + + return new CaptureChild( + process, + pipe, + reader, + writer, + response.TcpRedirectPort, + response.UdpRedirectPort, + response.DnsInterceptionActive, + response.DnsFailClosedActive, + response.ManagedRoutingActive, + response.ManagedRoutingRuleCount); + } + catch + { + writer?.Dispose(); + reader?.Dispose(); + pipe.Dispose(); + if (process is not null) + { + TryTerminate(process); + process.Dispose(); + } + throw; + } + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + _disposed = true; + + try + { + if (_pipe.IsConnected && !_process.HasExited) + { + await _writer.WriteLineAsync("{\"command\":\"stop\"}").ConfigureAwait(false); + Task readTask = _reader.ReadLineAsync(CancellationToken.None).AsTask(); + await Task.WhenAny(readTask, Task.Delay(TimeSpan.FromSeconds(2))).ConfigureAwait(false); + } + } + catch + { + } + + try + { + if (!_process.HasExited && !_process.WaitForExit(3000)) + { + _process.Kill(entireProcessTree: true); + _process.WaitForExit(3000); + } + } + catch + { + } + + _writer.Dispose(); + _reader.Dispose(); + _pipe.Dispose(); + _process.Dispose(); + } + + private static void TryTerminate(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + } + } +} diff --git a/Shadowsocks.NetworkService/CaptureChildSupervisor.cs b/Shadowsocks.NetworkService/CaptureChildSupervisor.cs new file mode 100644 index 000000000..46b58790b --- /dev/null +++ b/Shadowsocks.NetworkService/CaptureChildSupervisor.cs @@ -0,0 +1,386 @@ +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService; + +/// +/// Owns the elevated broker's capture child lifecycle and asynchronous recovery state. +/// The broker protocol stays responsive while restart backoff runs in the background. +/// +internal sealed class CaptureChildSupervisor : IAsyncDisposable +{ + private static readonly TimeSpan StableRunThreshold = TimeSpan.FromMinutes(1); + private static readonly TimeSpan[] RestartDelays = + [ + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(10), + ]; + + private readonly SemaphoreSlim _gate = new(1, 1); + private CancellationTokenSource _recoveryCancellation = new(); + private CaptureChild? _child; + private StartRequest? _lastRequest; + private Task? _recoveryTask; + private bool _gameMode; + private bool _captureFaulted; + private int _automaticRestartAttempts; + private bool _disposed; + + public async Task StartOrRestartAsync(StartRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + CancelRecoveryLocked(); + _lastRequest = request; + _captureFaulted = false; + _automaticRestartAttempts = 0; + + if (_gameMode) + { + return NetworkServiceResponses.Success("Configuration stored; Game Mode remains active."); + } + + await StopChildLockedAsync().ConfigureAwait(false); + try + { + CaptureChild child = await CaptureChild.StartAsync(request, cancellationToken).ConfigureAwait(false); + _child = child; + return NetworkServiceResponses.CaptureStatus(child, "Admin capture active."); + } + catch (Exception exception) + { + _captureFaulted = true; + return NetworkServiceResponses.Failure(exception.Message); + } + } + finally + { + _gate.Release(); + } + } + + public async Task EnterGameModeAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + CancelRecoveryLocked(); + _gameMode = true; + _captureFaulted = false; + _automaticRestartAttempts = 0; + await StopChildLockedAsync().ConfigureAwait(false); + + bool removed = DriverServiceCleaner.TryRemove(); + ServiceResponse response = NetworkServiceResponses.Success( + removed + ? "Game Mode active; WinDivert capture and driver removed." + : "Game Mode active; capture stopped, driver cleanup reported a warning."); + response.DriverRemoved = removed; + return response; + } + finally + { + _gate.Release(); + } + } + + public async Task ExitGameModeAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + CancelRecoveryLocked(); + _gameMode = false; + _captureFaulted = false; + _automaticRestartAttempts = 0; + + if (_lastRequest is null) + { + return NetworkServiceResponses.Failure("No Admin Mode configuration is available."); + } + + await StopChildLockedAsync().ConfigureAwait(false); + try + { + StartRequest request = _lastRequest; + CaptureChild child = await CaptureChild.StartAsync(request, cancellationToken).ConfigureAwait(false); + _child = child; + return NetworkServiceResponses.CaptureStatus(child, "Admin capture restored."); + } + catch (Exception exception) + { + _captureFaulted = true; + return NetworkServiceResponses.Failure(exception.Message); + } + } + finally + { + _gate.Release(); + } + } + + public async Task PingAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + if (_child is not null && _child.IsAlive + && DateTime.UtcNow - _child.StartedUtc >= StableRunThreshold) + { + _automaticRestartAttempts = 0; + } + + if (_child is not null && !_child.IsAlive) + { + await StopChildLockedAsync().ConfigureAwait(false); + } + + if (_child is not null && _child.IsAlive) + { + return NetworkServiceResponses.CaptureStatus(_child, "pong"); + } + + EnsureRecoveryScheduledLocked(); + return NetworkServiceResponses.Success( + _captureFaulted + ? "capture-faulted" + : _recoveryTask is { IsCompleted: false } + ? "capture-recovering" + : "pong"); + } + finally + { + _gate.Release(); + } + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_disposed) + { + return NetworkServiceResponses.Success("Stopping."); + } + + CancelRecoveryLocked(); + await StopChildLockedAsync().ConfigureAwait(false); + bool removed = DriverServiceCleaner.TryRemove(); + ServiceResponse response = NetworkServiceResponses.Success("Stopping."); + response.DriverRemoved = removed; + return response; + } + finally + { + _gate.Release(); + } + } + + private void EnsureRecoveryScheduledLocked() + { + if (_gameMode + || _captureFaulted + || _lastRequest is null + || _automaticRestartAttempts >= RestartDelays.Length + || _recoveryTask is { IsCompleted: false }) + { + return; + } + + StartRequest request = _lastRequest; + CancellationToken token = _recoveryCancellation.Token; + _recoveryTask = RecoverAsync(request, token); + } + + private async Task RecoverAsync(StartRequest request, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + int attempt; + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (!CanRecoverLocked(request)) + { + return; + } + + attempt = _automaticRestartAttempts; + if (attempt >= RestartDelays.Length) + { + _captureFaulted = true; + return; + } + _automaticRestartAttempts++; + } + finally + { + _gate.Release(); + } + + await Task.Delay(RestartDelays[attempt], cancellationToken).ConfigureAwait(false); + + CaptureChild? candidate = null; + try + { + // Starting a child can take up to the handshake timeout. Do it outside + // the supervisor gate so ping/stop/game-mode commands remain responsive. + candidate = await CaptureChild.StartAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return; + } + catch + { + await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false); + try + { + if (!CanRecoverLocked(request)) + { + return; + } + + if (_automaticRestartAttempts >= RestartDelays.Length) + { + _captureFaulted = true; + return; + } + } + finally + { + _gate.Release(); + } + + continue; + } + + bool accepted = false; + await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false); + try + { + if (candidate is not null + && !cancellationToken.IsCancellationRequested + && CanRecoverLocked(request)) + { + _child = candidate; + candidate = null; + accepted = true; + } + } + finally + { + _gate.Release(); + } + + if (candidate is not null) + { + await candidate.DisposeAsync().ConfigureAwait(false); + } + + if (accepted || cancellationToken.IsCancellationRequested) + { + return; + } + } + } + + private bool CanRecoverLocked(StartRequest request) + { + return !_disposed + && !_gameMode + && !_captureFaulted + && ReferenceEquals(_lastRequest, request) + && _child is null; + } + + private async Task StopChildLockedAsync() + { + CaptureChild? child = _child; + _child = null; + if (child is not null) + { + await child.DisposeAsync().ConfigureAwait(false); + } + } + + private void CancelRecoveryLocked() + { + CancellationTokenSource previousCancellation = _recoveryCancellation; + Task? previousTask = _recoveryTask; + previousCancellation.Cancel(); + _recoveryCancellation = new CancellationTokenSource(); + _recoveryTask = null; + _ = DisposeRecoveryResourcesAsync(previousTask, previousCancellation); + } + + private static async Task DisposeRecoveryResourcesAsync( + Task? recoveryTask, + CancellationTokenSource cancellation) + { + if (recoveryTask is not null) + { + try + { + await recoveryTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch + { + // Recovery failures are reflected through capture state; observing the + // task here prevents an unobserved background exception. + } + } + + cancellation.Dispose(); + } + + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(_disposed, this); + + public async ValueTask DisposeAsync() + { + Task? recoveryTask; + await _gate.WaitAsync().ConfigureAwait(false); + try + { + if (_disposed) + { + return; + } + + _disposed = true; + _recoveryCancellation.Cancel(); + recoveryTask = _recoveryTask; + await StopChildLockedAsync().ConfigureAwait(false); + } + finally + { + _gate.Release(); + } + + if (recoveryTask is not null) + { + try + { + await recoveryTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + } + + _recoveryCancellation.Dispose(); + _gate.Dispose(); + DriverServiceCleaner.TryRemove(); + } +} diff --git a/Shadowsocks.NetworkService/DriverServiceCleaner.cs b/Shadowsocks.NetworkService/DriverServiceCleaner.cs new file mode 100644 index 000000000..42b57b743 --- /dev/null +++ b/Shadowsocks.NetworkService/DriverServiceCleaner.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; + +namespace Shadowsocks.NetworkService; + +/// +/// Removes the WinDivert kernel service after every capture process has exited. +/// This is used by Game Mode so anti-cheat-sensitive applications do not see an +/// active WinDivert driver merely because Shadowsocks was previously in Admin Mode. +/// +internal static class DriverServiceCleaner +{ + private static readonly string[] ServiceNames = ["WinDivert", "WinDivert64"]; + + public static bool TryRemove() + { + bool clean = true; + foreach (string serviceName in ServiceNames) + { + RunSc("stop", serviceName); + int deleteExitCode = RunSc("delete", serviceName); + if (deleteExitCode is not (0 or 1060)) + { + // sc.exe normally returns a generic non-zero process code rather than + // the SCM error code, so failure here is advisory. The next WinDivertOpen + // can still recover an already stopped/marked-for-delete service. + clean = false; + } + } + + return clean; + } + + private static int RunSc(string verb, string serviceName) + { + try + { + using Process process = Process.Start(new ProcessStartInfo + { + FileName = Path.Combine(Environment.SystemDirectory, "sc.exe"), + Arguments = $"{verb} {serviceName}", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + if (process.WaitForExit(5000)) + { + return process.ExitCode; + } + + try + { + process.Kill(entireProcessTree: true); + process.WaitForExit(1000); + } + catch + { + } + + return -1; + } + catch + { + return -1; + } + } +} diff --git a/Shadowsocks.NetworkService/Ipc/Protocol.cs b/Shadowsocks.NetworkService/Ipc/Protocol.cs new file mode 100644 index 000000000..4a4a5ff94 --- /dev/null +++ b/Shadowsocks.NetworkService/Ipc/Protocol.cs @@ -0,0 +1,110 @@ +using System.Text.Json.Serialization; + +namespace Shadowsocks.NetworkService.Ipc; + +internal enum RouteAction +{ + Default = 0, + Proxy = 1, + Direct = 2, + Block = 3, + Deferred = 4, +} + +internal enum DnsPolicyMode +{ + System = 0, + Direct = 1, + Proxy = 2, + CustomDoh = 3, + DnsCrypt = 4, +} + +internal sealed class ApplicationRuleDto +{ + public bool Enabled { get; set; } = true; + public string Application { get; set; } = string.Empty; + public RouteAction Action { get; set; } = RouteAction.Proxy; +} + +internal sealed class DnsPolicyDto +{ + public DnsPolicyMode Mode { get; set; } = DnsPolicyMode.System; + public string DirectDnsServer { get; set; } = string.Empty; + public string DirectDnsFallbackServer { get; set; } = string.Empty; + public bool DirectDnsRouteThroughShadowsocks { get; set; } + public string CustomDohUrl { get; set; } = string.Empty; + public bool CustomDohRouteThroughShadowsocks { get; set; } + public int DnsCryptPort { get; set; } + public int DnsCryptProcessId { get; set; } + public string DnsCryptComponentRoot { get; set; } = string.Empty; + public bool FailClosed { get; set; } = true; + + [JsonIgnore] + public bool DirectDnsReady => Mode == DnsPolicyMode.Direct + && System.Net.IPAddress.TryParse(DirectDnsServer?.Trim(), out _) + && (string.IsNullOrWhiteSpace(DirectDnsFallbackServer) + || System.Net.IPAddress.TryParse(DirectDnsFallbackServer.Trim(), out _)); + + [JsonIgnore] + public bool DnsCryptReady => Mode == DnsPolicyMode.DnsCrypt + && DnsCryptPort is >= 1 and <= 65535 + && DnsCryptProcessId > 0; + + [JsonIgnore] + public bool CustomDohReady => Mode == DnsPolicyMode.CustomDoh + && Uri.TryCreate(CustomDohUrl, UriKind.Absolute, out Uri? uri) + && string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); +} + + +internal sealed class ManagedRoutingDto +{ + public bool Enabled { get; set; } + public string Source { get; set; } = string.Empty; + public List DefaultRules { get; set; } = []; + public List UserRules { get; set; } = []; +} + +internal sealed class StartRequest +{ + public string Command { get; set; } = "start"; + public string WinDivertDirectory { get; set; } = string.Empty; + public string LocalProxyHost { get; set; } = "127.0.0.1"; + public int LocalProxyPort { get; set; } = 1080; + public int MainProcessId { get; set; } + public RouteAction DefaultRoute { get; set; } = RouteAction.Direct; + public List ApplicationRules { get; set; } = []; + public List ExcludedProcessIds { get; set; } = []; + public ManagedRoutingDto ManagedRouting { get; set; } = new(); + public DnsPolicyDto DnsPolicy { get; set; } = new(); +} + +internal sealed class ControlRequest +{ + public string Command { get; set; } = string.Empty; +} + +internal sealed class ServiceResponse +{ + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public int TcpRedirectPort { get; set; } + public int UdpRedirectPort { get; set; } + public bool CaptureActive { get; set; } + public bool DnsInterceptionActive { get; set; } + public bool DnsFailClosedActive { get; set; } + public bool ManagedRoutingActive { get; set; } + public int ManagedRoutingRuleCount { get; set; } + public bool DriverRemoved { get; set; } +} + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(StartRequest))] +[JsonSerializable(typeof(ManagedRoutingDto))] +[JsonSerializable(typeof(ControlRequest))] +[JsonSerializable(typeof(ServiceResponse))] +internal partial class NetworkServiceJsonContext : JsonSerializerContext +{ +} diff --git a/Shadowsocks.NetworkService/NetworkServiceResponses.cs b/Shadowsocks.NetworkService/NetworkServiceResponses.cs new file mode 100644 index 000000000..f3e5adf6e --- /dev/null +++ b/Shadowsocks.NetworkService/NetworkServiceResponses.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService; + +internal static class NetworkServiceResponses +{ + private static string ServiceVersion => typeof(Program).Assembly.GetName().Version?.ToString(4) ?? "0.0.0.0"; + + public static ServiceResponse Success(string message) => new() + { + Success = true, + Message = message, + Version = ServiceVersion, + }; + + public static ServiceResponse Failure(string message) => new() + { + Success = false, + Message = message, + Version = ServiceVersion, + }; + + public static ServiceResponse CaptureStatus(CaptureChild child, string message) + { + bool active = child.IsAlive; + return new ServiceResponse + { + Success = true, + Message = message, + Version = ServiceVersion, + CaptureActive = active, + TcpRedirectPort = active ? child.TcpRedirectPort : 0, + UdpRedirectPort = active ? child.UdpRedirectPort : 0, + DnsInterceptionActive = active && child.DnsInterceptionActive, + DnsFailClosedActive = active && child.DnsFailClosedActive, + ManagedRoutingActive = active && child.ManagedRoutingActive, + ManagedRoutingRuleCount = active ? child.ManagedRoutingRuleCount : 0, + }; + } + + public static Task SendAsync(StreamWriter writer, ServiceResponse response) + => writer.WriteLineAsync(JsonSerializer.Serialize(response, NetworkServiceJsonContext.Default.ServiceResponse)); +} diff --git a/Shadowsocks.NetworkService/Program.cs b/Shadowsocks.NetworkService/Program.cs new file mode 100644 index 000000000..2e345a909 --- /dev/null +++ b/Shadowsocks.NetworkService/Program.cs @@ -0,0 +1,130 @@ +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.Routing; + +namespace Shadowsocks.NetworkService; + +internal static class Program +{ + private static async Task Main(string[] args) + { + string? startupLogPath = GetArgument(args, "--startup-log"); + string? capturePipeName = GetArgument(args, "--capture-pipe"); + if (!string.IsNullOrWhiteSpace(capturePipeName)) + { + return await RunCaptureChildAsync(capturePipeName).ConfigureAwait(false); + } + + string? pipeName = GetArgument(args, "--pipe"); + if (string.IsNullOrWhiteSpace(pipeName)) + { + return 2; + } + + return await BrokerHost.RunAsync(pipeName, startupLogPath).ConfigureAwait(false); + } + + private static async Task RunCaptureChildAsync(string pipeName) + { + try + { + using NamedPipeClientStream pipe = new( + ".", + pipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous); + using CancellationTokenSource connectTimeout = new(TimeSpan.FromSeconds(15)); + await pipe.ConnectAsync(connectTimeout.Token).ConfigureAwait(false); + + using StreamReader reader = new(pipe, Encoding.UTF8, false, 4096, leaveOpen: true); + using StreamWriter writer = new(pipe, new UTF8Encoding(false), 4096, leaveOpen: true) + { + AutoFlush = true, + }; + + string? startLine = await reader.ReadLineAsync().ConfigureAwait(false); + StartRequest? request = string.IsNullOrWhiteSpace(startLine) + ? null + : JsonSerializer.Deserialize(startLine, NetworkServiceJsonContext.Default.StartRequest); + if (request is null) + { + await NetworkServiceResponses.SendAsync( + writer, + NetworkServiceResponses.Failure("Invalid capture child request.")).ConfigureAwait(false); + return 3; + } + + await using WinDivertTransparentRouter router = new(request); + try + { + router.Start(); + } + catch (Exception exception) + { + await NetworkServiceResponses.SendAsync( + writer, + NetworkServiceResponses.Failure(exception.Message)).ConfigureAwait(false); + return 5; + } + + await NetworkServiceResponses.SendAsync(writer, new ServiceResponse + { + Success = true, + Message = "capture-ready", + TcpRedirectPort = router.TcpRedirectPort, + UdpRedirectPort = router.UdpRedirectPort, + CaptureActive = true, + DnsInterceptionActive = router.DnsInterceptionActive, + DnsFailClosedActive = router.DnsFailClosedActive, + ManagedRoutingActive = router.ManagedRoutingActive, + ManagedRoutingRuleCount = router.ManagedRoutingRuleCount, + }).ConfigureAwait(false); + + while (true) + { + Task readTask = reader.ReadLineAsync(); + Task completed = await Task.WhenAny(readTask, router.Completion).ConfigureAwait(false); + if (completed == router.Completion) + { + await router.Completion.ConfigureAwait(false); + return 6; + } + + string? line = await readTask.ConfigureAwait(false); + if (line is null) + { + break; + } + + ControlRequest? control = JsonSerializer.Deserialize(line, NetworkServiceJsonContext.Default.ControlRequest); + if (string.Equals(control?.Command, "stop", StringComparison.OrdinalIgnoreCase)) + { + await NetworkServiceResponses.SendAsync( + writer, + NetworkServiceResponses.Success("capture-stopping")).ConfigureAwait(false); + break; + } + } + + return 0; + } + catch + { + return 1; + } + } + + private static string? GetArgument(string[] args, string name) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } +} diff --git a/Shadowsocks.NetworkService/Properties/AssemblyInfo.cs b/Shadowsocks.NetworkService/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e65fc41b2 --- /dev/null +++ b/Shadowsocks.NetworkService/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Shadowsocks.UnitTests")] diff --git a/Shadowsocks.NetworkService/Routing/ApplicationPolicy.cs b/Shadowsocks.NetworkService/Routing/ApplicationPolicy.cs new file mode 100644 index 000000000..f466746f8 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/ApplicationPolicy.cs @@ -0,0 +1,215 @@ +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService.Routing; + +internal sealed class ApplicationPolicy +{ + private const string DnsCryptExecutableName = "dnscrypt-proxy.exe"; + private readonly IReadOnlyList _rules; + private readonly int _mainProcessId; + private readonly HashSet _excludedProcessIds; + private readonly RouteAction _defaultRoute; + private readonly string? _dnsCryptComponentRoot; + private readonly int _dnsCryptProcessId; + private readonly DnsPolicyMode _dnsPolicyMode; + private readonly bool _managedRoutingEnabled; + + public ApplicationPolicy(StartRequest request) + { + _rules = request.ApplicationRules ?? []; + _mainProcessId = request.MainProcessId; + _excludedProcessIds = request.ExcludedProcessIds is null ? [] : [.. request.ExcludedProcessIds]; + _defaultRoute = request.DefaultRoute is RouteAction.Direct or RouteAction.Block + ? request.DefaultRoute + : RouteAction.Proxy; + _dnsCryptComponentRoot = NormalizeDirectory(request.DnsPolicy?.DnsCryptComponentRoot); + _dnsCryptProcessId = request.DnsPolicy?.DnsCryptProcessId ?? 0; + _dnsPolicyMode = request.DnsPolicy?.Mode ?? DnsPolicyMode.System; + _managedRoutingEnabled = request.ManagedRouting?.Enabled == true; + } + + public bool IsExcludedProcess(int processId, string? processPath = null) + => processId == Environment.ProcessId + || processId == _mainProcessId + || _excludedProcessIds.Contains(processId) + || IsManagedDnsCryptExecutable(processPath); + + /// + /// Determines whether a process may bypass transparent port-53 policy. In DNSCrypt mode, + /// Shadowsocks and SIP003 remain excluded from generic traffic capture but their DNS/53 is + /// still intercepted; only NetworkService and the managed dnscrypt-proxy runtime are exempt + /// to prevent the local DNS bridge from recursively capturing itself. + /// + public bool IsDnsInterceptionExempt(int processId, string? processPath = null) + { + // In DNSCrypt mode, generic capture exclusions (Shadowsocks itself and SIP003 + // plugins) must NOT become DNS exclusions. Otherwise their own UDP/TCP 53 + // traffic can escape in plaintext while the rest of the system is protected. + // The NetworkService process and the managed dnscrypt-proxy process remain + // exempt so the local transparent bridge cannot recursively capture itself. + if (_dnsPolicyMode == DnsPolicyMode.DnsCrypt) + { + return processId == Environment.ProcessId + || (_dnsCryptProcessId > 0 && processId == _dnsCryptProcessId) + || IsManagedDnsCryptExecutable(processPath); + } + + return processId == Environment.ProcessId + || processId == _mainProcessId + || _excludedProcessIds.Contains(processId) + || (_dnsCryptProcessId > 0 && processId == _dnsCryptProcessId) + || IsManagedDnsCryptExecutable(processPath); + } + + public RouteAction Evaluate( + byte protocol, + ushort remotePort, + int processId, + string? processPath, + string? processName) + { + if (IsExcludedProcess(processId, processPath)) + { + return RouteAction.Direct; + } + + foreach (ApplicationRuleDto rule in _rules) + { + if (!rule.Enabled || string.IsNullOrWhiteSpace(rule.Application)) + { + continue; + } + + if (Matches(rule.Application.Trim(), processPath, processName)) + { + if (rule.Action != RouteAction.Default) + { + return rule.Action; + } + + // Default means "continue through the shared routing policy", matching + // TrafficPolicyEngine in the main process rather than bypassing ABP rules. + break; + } + } + + // Only ordinary TCP is deferred for Host/SNI inspection. DNS has its own policy, + // UDP has no reliable hostname at this layer, and explicit application rules above + // remain authoritative without any payload inspection. + if (_managedRoutingEnabled && protocol == 6 && remotePort != 53) + { + return RouteAction.Deferred; + } + + return _defaultRoute; + } + + public RouteAction DefaultRoute => _defaultRoute; + + private bool IsManagedDnsCryptExecutable(string? processPath) + { + if (string.IsNullOrWhiteSpace(_dnsCryptComponentRoot) || string.IsNullOrWhiteSpace(processPath)) + { + return false; + } + + try + { + string fullPath = Path.GetFullPath(processPath); + if (!string.Equals(Path.GetFileName(fullPath), DnsCryptExecutableName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string directory = Path.GetDirectoryName(fullPath) ?? string.Empty; + string normalizedDirectory = NormalizeDirectory(directory) ?? string.Empty; + return normalizedDirectory.StartsWith(_dnsCryptComponentRoot, StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + private static string? NormalizeDirectory(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + try + { + string full = Path.GetFullPath(path.Trim()); + return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + } + catch + { + return null; + } + } + + private static bool Matches(string pattern, string? processPath, string? processName) + { + string path = processPath ?? string.Empty; + string name = processName ?? (string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileName(path)); + string fileName = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileName(path); + string nameWithExe = string.IsNullOrEmpty(name) || name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ? name + : name + ".exe"; + if (!pattern.Contains('*') && !pattern.Contains('?')) + { + string patternFileName = Path.GetFileName(pattern); + return string.Equals(pattern, path, StringComparison.OrdinalIgnoreCase) + || string.Equals(pattern, fileName, StringComparison.OrdinalIgnoreCase) + || string.Equals(pattern, name, StringComparison.OrdinalIgnoreCase) + || string.Equals(pattern, nameWithExe, StringComparison.OrdinalIgnoreCase) + || string.Equals(patternFileName, name, StringComparison.OrdinalIgnoreCase) + || string.Equals(patternFileName, nameWithExe, StringComparison.OrdinalIgnoreCase); + } + + return WildcardMatch(path, pattern) + || WildcardMatch(fileName, pattern) + || WildcardMatch(name, pattern) + || WildcardMatch(nameWithExe, pattern); + } + + private static bool WildcardMatch(string value, string pattern) + { + int valueIndex = 0; + int patternIndex = 0; + int starIndex = -1; + int checkpoint = 0; + while (valueIndex < value.Length) + { + if (patternIndex < pattern.Length + && (pattern[patternIndex] == '?' + || char.ToUpperInvariant(pattern[patternIndex]) == char.ToUpperInvariant(value[valueIndex]))) + { + valueIndex++; + patternIndex++; + } + else if (patternIndex < pattern.Length && pattern[patternIndex] == '*') + { + starIndex = patternIndex++; + checkpoint = valueIndex; + } + else if (starIndex >= 0) + { + patternIndex = starIndex + 1; + valueIndex = ++checkpoint; + } + else + { + return false; + } + } + + while (patternIndex < pattern.Length && pattern[patternIndex] == '*') + { + patternIndex++; + } + + return patternIndex == pattern.Length; + } +} diff --git a/Shadowsocks.NetworkService/Routing/DnsRoutingPolicy.cs b/Shadowsocks.NetworkService/Routing/DnsRoutingPolicy.cs new file mode 100644 index 000000000..d91942083 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/DnsRoutingPolicy.cs @@ -0,0 +1,38 @@ +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService.Routing; + +internal enum DnsRouteDecision +{ + Direct = 0, + Redirect = 1, + Block = 2, + Proxy = 3, +} + +internal static class DnsRoutingPolicy +{ + public static DnsRouteDecision Evaluate(DnsPolicyDto? policy, bool dnsInterceptionExempt) + { + if (dnsInterceptionExempt || policy is null) + return DnsRouteDecision.Direct; + + return policy.Mode switch + { + DnsPolicyMode.System => DnsRouteDecision.Direct, + DnsPolicyMode.Direct => policy.DirectDnsReady + ? DnsRouteDecision.Redirect + : policy.DirectDnsRouteThroughShadowsocks ? DnsRouteDecision.Proxy : DnsRouteDecision.Direct, + DnsPolicyMode.Proxy => DnsRouteDecision.Proxy, + DnsPolicyMode.CustomDoh => policy.CustomDohReady + ? DnsRouteDecision.Redirect + : policy.FailClosed ? DnsRouteDecision.Block : DnsRouteDecision.Direct, + // DNSCrypt is always fail-closed. Falling back to Direct here would leak + // plaintext DNS during startup/restart windows and contradict the selected mode. + DnsPolicyMode.DnsCrypt => policy.DnsCryptReady + ? DnsRouteDecision.Redirect + : DnsRouteDecision.Block, + _ => DnsRouteDecision.Direct, + }; + } +} diff --git a/Shadowsocks.NetworkService/Routing/FlowModels.cs b/Shadowsocks.NetworkService/Routing/FlowModels.cs new file mode 100644 index 000000000..cae3486b6 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/FlowModels.cs @@ -0,0 +1,31 @@ +using System.Net; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService.Routing; + +internal readonly record struct FlowKey( + byte Protocol, + IPAddress LocalAddress, + ushort LocalPort, + IPAddress RemoteAddress, + ushort RemotePort); + +internal readonly record struct ReflectedFlowKey( + byte Protocol, + IPAddress LocalAddress, + ushort ApplicationPort, + IPAddress RemoteAddress); + +internal readonly record struct PeerFlowKey( + byte Protocol, + ushort ApplicationPort, + IPAddress RemoteAddress); + +internal sealed record FlowState( + FlowKey Key, + int ProcessId, + string? ProcessPath, + string? ProcessName, + RouteAction Route, + RouteAction FallbackRoute, + DateTime LastSeenUtc); diff --git a/Shadowsocks.NetworkService/Routing/FlowRegistry.cs b/Shadowsocks.NetworkService/Routing/FlowRegistry.cs new file mode 100644 index 000000000..374c619a7 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/FlowRegistry.cs @@ -0,0 +1,232 @@ +using System.Collections.Concurrent; +using System.Net; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService.Routing; + +internal sealed class FlowRegistry +{ + private readonly ConcurrentDictionary _flows = new(); + private readonly ConcurrentDictionary _reflected = new(); + private readonly ConcurrentDictionary> _reflectedByPeer = new(); + private readonly ApplicationPolicy _policy; + private long _lastSweepTicks; + + public FlowRegistry(ApplicationPolicy policy) + { + _policy = policy; + } + + public FlowState GetOrCreate(FlowKey key) + { + FlowState state = _flows.GetOrAdd(key, CreateState); + if (state.ProcessId <= 0 && key.RemotePort == 53) + { + ProcessIdentity process = ProcessResolver.Resolve(key); + if (process.ProcessId > 0) + { + FlowState resolved = state with + { + ProcessId = process.ProcessId, + ProcessPath = process.ProcessPath, + ProcessName = process.ProcessName, + Route = _policy.Evaluate( + key.Protocol, + key.RemotePort, + process.ProcessId, + process.ProcessPath, + process.ProcessName), + }; + if (_flows.TryUpdate(key, resolved, state)) + { + state = resolved; + if (resolved.Route is RouteAction.Proxy or RouteAction.Deferred) + { + IndexReflected(resolved); + } + else + { + RemoveReflected(key); + } + } + else if (_flows.TryGetValue(key, out FlowState? current) && current is not null) + { + state = current; + } + } + } + + DateTime now = DateTime.UtcNow; + if ((now - state.LastSeenUtc) > TimeSpan.FromSeconds(1)) + { + FlowState updated = state with { LastSeenUtc = now }; + if (_flows.TryUpdate(key, updated, state)) + { + state = updated; + if (state.Route is RouteAction.Proxy or RouteAction.Deferred) + { + IndexReflected(state); + } + } + else if (_flows.TryGetValue(key, out FlowState? current) && current is not null) + { + state = current; + } + } + + MaybeSweep(now); + return state; + } + + public bool IsExcludedProcess(FlowState state) + => state is not null && _policy.IsExcludedProcess(state.ProcessId, state.ProcessPath); + + public bool IsDnsInterceptionExempt(FlowState state) + => state is not null && _policy.IsDnsInterceptionExempt(state.ProcessId, state.ProcessPath); + + public void MarkReflected(FlowState state) + { + ArgumentNullException.ThrowIfNull(state); + IndexReflected(state); + } + + public bool TryGetReflected( + byte protocol, + IPAddress localAddress, + ushort applicationPort, + IPAddress remoteAddress, + out FlowState? state) + { + ReflectedFlowKey exactKey = new(protocol, localAddress, applicationPort, remoteAddress); + if (_reflected.TryGetValue(exactKey, out state)) + { + return true; + } + + // UDP sockets bound to ANY may cause the transparent relay reply to use a + // different local interface. Keep an O(1) peer index for that fallback. + return TryGetByPeer(protocol, remoteAddress, applicationPort, out state); + } + + public bool TryGetByPeer( + byte protocol, + IPAddress remoteAddress, + ushort applicationPort, + out FlowState? state) + { + PeerFlowKey peerKey = new(protocol, applicationPort, remoteAddress); + if (!_reflectedByPeer.TryGetValue(peerKey, out ConcurrentDictionary? bucket) + || bucket.IsEmpty) + { + state = null; + return false; + } + + // More than one local interface can legitimately produce the same peer tuple. + // Keep those rare collisions inside a small peer bucket instead of scanning every flow. + state = bucket.Values.OrderByDescending(candidate => candidate.LastSeenUtc).FirstOrDefault(); + return state is not null; + } + + private FlowState CreateState(FlowKey key) + { + ProcessIdentity process = ProcessResolver.Resolve(key); + RouteAction route = _policy.Evaluate( + key.Protocol, + key.RemotePort, + process.ProcessId, + process.ProcessPath, + process.ProcessName); + FlowState state = new( + key, + process.ProcessId, + process.ProcessPath, + process.ProcessName, + route, + _policy.DefaultRoute, + DateTime.UtcNow); + if (route is RouteAction.Proxy or RouteAction.Deferred) + { + IndexReflected(state); + } + + return state; + } + + private static ReflectedFlowKey ToReflectedKey(FlowKey key) + => new(key.Protocol, key.LocalAddress, key.LocalPort, key.RemoteAddress); + + private static PeerFlowKey ToPeerKey(FlowKey key) + => new(key.Protocol, key.LocalPort, key.RemoteAddress); + + private void IndexReflected(FlowState state) + { + ReflectedFlowKey reflectedKey = ToReflectedKey(state.Key); + PeerFlowKey peerKey = ToPeerKey(state.Key); + _reflected[reflectedKey] = state; + ConcurrentDictionary bucket = _reflectedByPeer.GetOrAdd( + peerKey, + static _ => new ConcurrentDictionary()); + bucket[reflectedKey] = state; + } + + private void RemoveReflected(FlowKey key) + { + ReflectedFlowKey reflectedKey = ToReflectedKey(key); + PeerFlowKey peerKey = ToPeerKey(key); + _reflected.TryRemove(reflectedKey, out _); + if (_reflectedByPeer.TryGetValue(peerKey, out ConcurrentDictionary? bucket)) + { + bucket.TryRemove(reflectedKey, out _); + RemovePeerBucketIfEmpty(peerKey, bucket); + } + } + + private void RemoveReflectedIfMatches(FlowState state) + { + ReflectedFlowKey reflectedKey = ToReflectedKey(state.Key); + PeerFlowKey peerKey = ToPeerKey(state.Key); + _reflected.TryRemove(new KeyValuePair(reflectedKey, state)); + if (_reflectedByPeer.TryGetValue(peerKey, out ConcurrentDictionary? bucket)) + { + bucket.TryRemove(new KeyValuePair(reflectedKey, state)); + RemovePeerBucketIfEmpty(peerKey, bucket); + } + } + + private void RemovePeerBucketIfEmpty( + PeerFlowKey peerKey, + ConcurrentDictionary bucket) + { + if (bucket.IsEmpty) + { + _reflectedByPeer.TryRemove( + new KeyValuePair>(peerKey, bucket)); + } + } + + private void MaybeSweep(DateTime now) + { + long ticks = now.Ticks; + long previous = Interlocked.Read(ref _lastSweepTicks); + if (ticks - previous < TimeSpan.FromSeconds(30).Ticks + || Interlocked.CompareExchange(ref _lastSweepTicks, ticks, previous) != previous) + { + return; + } + + foreach ((FlowKey key, FlowState state) in _flows) + { + TimeSpan timeout = key.Protocol == 17 ? TimeSpan.FromMinutes(2) : TimeSpan.FromMinutes(15); + if (now - state.LastSeenUtc <= timeout) + { + continue; + } + + if (_flows.TryRemove(new KeyValuePair(key, state))) + { + RemoveReflectedIfMatches(state); + } + } + } +} diff --git a/Shadowsocks.NetworkService/Routing/InitialTcpProtocolInspector.cs b/Shadowsocks.NetworkService/Routing/InitialTcpProtocolInspector.cs new file mode 100644 index 000000000..adadfb54c --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/InitialTcpProtocolInspector.cs @@ -0,0 +1,403 @@ +using System.Buffers.Binary; +using System.Text; + +namespace Shadowsocks.NetworkService.Routing; + +internal enum TcpDestinationInspectionStatus +{ + NeedMoreData = 0, + Identified = 1, + Unknown = 2, +} + +internal readonly record struct TcpDestinationIdentity(string Host, string Url, string Protocol); + +/// +/// Extracts only clear-text routing metadata from the beginning of an intercepted TCP stream. +/// HTTP Host and TLS ClientHello SNI are sufficient for domain policy without decrypting TLS, +/// installing a CA certificate, or modifying application payloads. +/// +internal static class InitialTcpProtocolInspector +{ + private const int MaxHttpHeaderBytes = 64 * 1024; + private const int MaxTlsHelloBytes = 64 * 1024; + + private static readonly string[] HttpMethods = + [ + "GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS ", "PATCH ", + "TRACE ", "CONNECT ", + ]; + + public static TcpDestinationInspectionStatus Inspect( + ReadOnlySpan bytes, + ushort remotePort, + out TcpDestinationIdentity identity) + { + identity = default; + if (bytes.IsEmpty) + { + return TcpDestinationInspectionStatus.NeedMoreData; + } + + if (LooksLikeHttpPrefix(bytes)) + { + return InspectHttp(bytes, remotePort, out identity); + } + + if (bytes[0] == 0x16) + { + return InspectTls(bytes, remotePort, out identity); + } + + return TcpDestinationInspectionStatus.Unknown; + } + + private static TcpDestinationInspectionStatus InspectHttp( + ReadOnlySpan bytes, + ushort remotePort, + out TcpDestinationIdentity identity) + { + identity = default; + int headerEnd = bytes.IndexOf("\r\n\r\n"u8); + if (headerEnd < 0) + { + return bytes.Length >= MaxHttpHeaderBytes + ? TcpDestinationInspectionStatus.Unknown + : TcpDestinationInspectionStatus.NeedMoreData; + } + + string headers = Encoding.Latin1.GetString(bytes[..(headerEnd + 4)]); + int firstLineEnd = headers.IndexOf("\r\n", StringComparison.Ordinal); + if (firstLineEnd <= 0) + { + return TcpDestinationInspectionStatus.Unknown; + } + + string[] requestParts = headers[..firstLineEnd] + .Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); + if (requestParts.Length != 3) + { + return TcpDestinationInspectionStatus.Unknown; + } + + string method = requestParts[0]; + string target = requestParts[1]; + if (method.Equals("CONNECT", StringComparison.OrdinalIgnoreCase)) + { + if (!TryNormalizeAuthority(target, remotePort == 0 ? 443 : remotePort, 443, out string host, out string authority)) + { + return TcpDestinationInspectionStatus.Unknown; + } + + identity = new TcpDestinationIdentity(host, "https://" + authority + "/", "http-connect"); + return TcpDestinationInspectionStatus.Identified; + } + + if (Uri.TryCreate(target, UriKind.Absolute, out Uri? absolute) + && (absolute.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) + || absolute.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))) + { + identity = new TcpDestinationIdentity(absolute.Host.TrimEnd('.'), absolute.AbsoluteUri, "http"); + return TcpDestinationInspectionStatus.Identified; + } + + string? hostHeader = null; + int cursor = firstLineEnd + 2; + while (cursor < headerEnd) + { + int next = headers.IndexOf("\r\n", cursor, StringComparison.Ordinal); + if (next < 0 || next > headerEnd) + { + break; + } + + ReadOnlySpan line = headers.AsSpan(cursor, next - cursor); + int colon = line.IndexOf(':'); + if (colon > 0 && line[..colon].Trim().ToString().Equals("Host", StringComparison.OrdinalIgnoreCase)) + { + hostHeader = line[(colon + 1)..].Trim().ToString(); + break; + } + cursor = next + 2; + } + + if (string.IsNullOrWhiteSpace(hostHeader) + || !TryNormalizeAuthority(hostHeader, remotePort == 0 ? 80 : remotePort, 80, out string requestHost, out string requestAuthority)) + { + return TcpDestinationInspectionStatus.Unknown; + } + + string path = target.StartsWith('/') ? target : "/"; + identity = new TcpDestinationIdentity(requestHost, "http://" + requestAuthority + path, "http"); + return TcpDestinationInspectionStatus.Identified; + } + + private static TcpDestinationInspectionStatus InspectTls( + ReadOnlySpan bytes, + ushort remotePort, + out TcpDestinationIdentity identity) + { + identity = default; + if (bytes.Length > MaxTlsHelloBytes) + { + return TcpDestinationInspectionStatus.Unknown; + } + + List handshake = []; + int recordOffset = 0; + while (true) + { + if (bytes.Length - recordOffset < 5) + { + return TcpDestinationInspectionStatus.NeedMoreData; + } + + byte contentType = bytes[recordOffset]; + if (contentType != 0x16) + { + return TcpDestinationInspectionStatus.Unknown; + } + + int recordLength = BinaryPrimitives.ReadUInt16BigEndian(bytes.Slice(recordOffset + 3, 2)); + int payloadOffset = recordOffset + 5; + if (recordLength <= 0 || bytes.Length - payloadOffset < recordLength) + { + return TcpDestinationInspectionStatus.NeedMoreData; + } + + handshake.AddRange(bytes.Slice(payloadOffset, recordLength).ToArray()); + if (handshake.Count >= 4) + { + if (handshake[0] != 0x01) + { + return TcpDestinationInspectionStatus.Unknown; + } + + int helloLength = (handshake[1] << 16) | (handshake[2] << 8) | handshake[3]; + if (helloLength <= 0 || helloLength > MaxTlsHelloBytes) + { + return TcpDestinationInspectionStatus.Unknown; + } + + if (handshake.Count >= helloLength + 4) + { + ReadOnlySpan hello = handshake.ToArray().AsSpan(4, helloLength); + if (!TryReadServerName(hello, out string host)) + { + return TcpDestinationInspectionStatus.Unknown; + } + + string authority = remotePort is 0 or 443 + ? host + : host + ":" + remotePort.ToString(System.Globalization.CultureInfo.InvariantCulture); + identity = new TcpDestinationIdentity(host, "https://" + authority + "/", "tls-sni"); + return TcpDestinationInspectionStatus.Identified; + } + } + + recordOffset = payloadOffset + recordLength; + if (recordOffset >= bytes.Length) + { + return TcpDestinationInspectionStatus.NeedMoreData; + } + } + } + + private static bool TryReadServerName(ReadOnlySpan hello, out string host) + { + host = string.Empty; + int offset = 0; + if (!Skip(hello, ref offset, 2 + 32)) // legacy_version + random + { + return false; + } + + if (!TryReadByteVector(hello, ref offset, out _) + || !TryReadUInt16Vector(hello, ref offset, out _) + || !TryReadByteVector(hello, ref offset, out _)) + { + return false; + } + + if (offset == hello.Length) + { + return false; // ClientHello without extensions/SNI. + } + + if (!TryReadUInt16Vector(hello, ref offset, out ReadOnlySpan extensions)) + { + return false; + } + + int extensionOffset = 0; + while (extensionOffset + 4 <= extensions.Length) + { + ushort type = BinaryPrimitives.ReadUInt16BigEndian(extensions.Slice(extensionOffset, 2)); + int length = BinaryPrimitives.ReadUInt16BigEndian(extensions.Slice(extensionOffset + 2, 2)); + extensionOffset += 4; + if (length < 0 || extensionOffset + length > extensions.Length) + { + return false; + } + + if (type == 0x0000) + { + ReadOnlySpan serverNames = extensions.Slice(extensionOffset, length); + if (serverNames.Length < 2) + { + return false; + } + + int listLength = BinaryPrimitives.ReadUInt16BigEndian(serverNames[..2]); + if (listLength > serverNames.Length - 2) + { + return false; + } + + int nameOffset = 2; + int listEnd = 2 + listLength; + while (nameOffset + 3 <= listEnd) + { + byte nameType = serverNames[nameOffset]; + int nameLength = BinaryPrimitives.ReadUInt16BigEndian(serverNames.Slice(nameOffset + 1, 2)); + nameOffset += 3; + if (nameLength <= 0 || nameOffset + nameLength > listEnd) + { + return false; + } + + if (nameType == 0) + { + string candidate = Encoding.ASCII.GetString(serverNames.Slice(nameOffset, nameLength)).Trim().TrimEnd('.'); + if (IsUsableHost(candidate)) + { + host = candidate; + return true; + } + } + nameOffset += nameLength; + } + } + + extensionOffset += length; + } + + return false; + } + + private static bool TryReadByteVector(ReadOnlySpan source, ref int offset, out ReadOnlySpan value) + { + value = default; + if (offset >= source.Length) + { + return false; + } + + int length = source[offset++]; + if (offset + length > source.Length) + { + return false; + } + + value = source.Slice(offset, length); + offset += length; + return true; + } + + private static bool TryReadUInt16Vector(ReadOnlySpan source, ref int offset, out ReadOnlySpan value) + { + value = default; + if (offset + 2 > source.Length) + { + return false; + } + + int length = BinaryPrimitives.ReadUInt16BigEndian(source.Slice(offset, 2)); + offset += 2; + if (offset + length > source.Length) + { + return false; + } + + value = source.Slice(offset, length); + offset += length; + return true; + } + + private static bool Skip(ReadOnlySpan source, ref int offset, int length) + { + if (length < 0 || offset + length > source.Length) + { + return false; + } + offset += length; + return true; + } + + private static bool LooksLikeHttpPrefix(ReadOnlySpan bytes) + { + foreach (string method in HttpMethods) + { + ReadOnlySpan ascii = Encoding.ASCII.GetBytes(method); + int comparable = Math.Min(bytes.Length, ascii.Length); + if (bytes[..comparable].SequenceEqual(ascii[..comparable])) + { + return true; + } + } + return false; + } + + private static bool TryNormalizeAuthority( + string value, + int transportPort, + int schemeDefaultPort, + out string host, + out string authority) + { + host = string.Empty; + authority = string.Empty; + string candidate = value.Trim(); + if (candidate.Length == 0) + { + return false; + } + + if (!Uri.TryCreate("http://" + candidate, UriKind.Absolute, out Uri? uri) + || !IsUsableHost(uri.Host)) + { + return false; + } + + host = uri.Host.TrimEnd('.'); + bool explicitPort = HasExplicitPort(candidate); + int port = explicitPort ? uri.Port : transportPort; + bool defaultForScheme = port == schemeDefaultPort; + if (host.Contains(':')) + { + authority = defaultForScheme ? "[" + host + "]" : "[" + host + "]:" + port; + } + else + { + authority = defaultForScheme ? host : host + ":" + port; + } + return true; + } + + + private static bool HasExplicitPort(string authority) + { + if (authority.StartsWith('[')) + { + int close = authority.IndexOf(']'); + return close >= 0 && close + 1 < authority.Length && authority[close + 1] == ':'; + } + + int firstColon = authority.IndexOf(':'); + return firstColon >= 0 && firstColon == authority.LastIndexOf(':'); + } + + private static bool IsUsableHost(string host) + => !string.IsNullOrWhiteSpace(host) + && Uri.CheckHostName(host.Trim().TrimEnd('.')) != UriHostNameType.Unknown; +} diff --git a/Shadowsocks.NetworkService/Routing/IpFragmentTracker.cs b/Shadowsocks.NetworkService/Routing/IpFragmentTracker.cs new file mode 100644 index 000000000..0d7f84124 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/IpFragmentTracker.cs @@ -0,0 +1,214 @@ +#nullable enable +using System.Buffers.Binary; +using System.Net; +using Shadowsocks.NetworkService.Ipc; + +namespace Shadowsocks.NetworkService.Routing; + +internal enum FragmentDisposition +{ + Direct = 0, + Drop = 1, +} + +internal readonly record struct IpFragmentKey( + byte Protocol, + IPAddress SourceAddress, + IPAddress DestinationAddress, + uint Identification); + +internal readonly record struct IpFragmentInfo( + IpFragmentKey Key, + bool IsFirstFragment, + bool MoreFragments); + +/// +/// Keeps all fragments of an outbound IP datagram on the same security path. +/// Fragmented datagrams that would require transparent address/port rewriting +/// are dropped as a whole; rewriting only the first fragment would otherwise +/// leak or corrupt the remaining plaintext fragments. +/// +internal sealed class IpFragmentTracker +{ + private static readonly TimeSpan EntryLifetime = TimeSpan.FromSeconds(30); + private readonly Dictionary _entries = new(); + private DateTime _lastSweepUtc; + + public void Record(IpFragmentInfo fragment, FragmentDisposition disposition, DateTime nowUtc) + { + _entries[fragment.Key] = new Entry(disposition, nowUtc); + MaybeSweep(nowUtc); + } + + public bool TryGetDisposition( + IpFragmentInfo fragment, + DateTime nowUtc, + out FragmentDisposition disposition) + { + MaybeSweep(nowUtc); + if (!_entries.TryGetValue(fragment.Key, out Entry entry)) + { + disposition = FragmentDisposition.Drop; + return false; + } + + disposition = entry.Disposition; + _entries[fragment.Key] = entry with { LastSeenUtc = nowUtc }; + if (!fragment.MoreFragments) + { + _entries.Remove(fragment.Key); + } + return true; + } + + internal static FragmentDisposition ForDnsDecision(DnsRouteDecision decision) + => decision == DnsRouteDecision.Direct ? FragmentDisposition.Direct : FragmentDisposition.Drop; + + internal static FragmentDisposition ForRoute(RouteAction route) + => route is RouteAction.Direct or RouteAction.Default + ? FragmentDisposition.Direct + : FragmentDisposition.Drop; + + internal static bool TryParse(ReadOnlySpan packet, out IpFragmentInfo fragment) + { + fragment = default; + if (packet.Length < 1) + return false; + + return (packet[0] >> 4) switch + { + 4 => TryParseIpv4(packet, out fragment), + 6 => TryParseIpv6(packet, out fragment), + _ => false, + }; + } + + private static bool TryParseIpv4(ReadOnlySpan packet, out IpFragmentInfo fragment) + { + fragment = default; + if (packet.Length < 20) + return false; + + int headerLength = (packet[0] & 0x0F) * 4; + if (headerLength < 20 || packet.Length < headerLength) + return false; + + ushort flagsAndOffset = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(6, 2)); + int fragmentOffset = flagsAndOffset & 0x1FFF; + bool moreFragments = (flagsAndOffset & 0x2000) != 0; + if (fragmentOffset == 0 && !moreFragments) + return false; + + byte protocol = packet[9]; + if (protocol is not 6 and not 17) + return false; + + uint identification = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(4, 2)); + fragment = new IpFragmentInfo( + new IpFragmentKey( + protocol, + new IPAddress(packet.Slice(12, 4)), + new IPAddress(packet.Slice(16, 4)), + identification), + fragmentOffset == 0, + moreFragments); + return true; + } + + private static bool TryParseIpv6(ReadOnlySpan packet, out IpFragmentInfo fragment) + { + fragment = default; + if (packet.Length < 40) + return false; + + byte nextHeader = packet[6]; + int offset = 40; + for (int extensionCount = 0; extensionCount < 8; extensionCount++) + { + if (nextHeader == 44) + { + if (packet.Length < offset + 8) + return false; + + byte fragmentProtocol = packet[offset]; + if (!IsTrackedIpv6FragmentProtocol(fragmentProtocol)) + return false; + + ushort offsetAndFlags = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(offset + 2, 2)); + int fragmentOffset = (offsetAndFlags & 0xFFF8) >> 3; + bool moreFragments = (offsetAndFlags & 0x0001) != 0; + if (fragmentOffset == 0 && !moreFragments) + return false; // IPv6 atomic fragment: no cross-packet state is required. + + uint identification = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(offset + 4, 4)); + fragment = new IpFragmentInfo( + new IpFragmentKey( + fragmentProtocol, + new IPAddress(packet.Slice(8, 16)), + new IPAddress(packet.Slice(24, 16)), + identification), + fragmentOffset == 0, + moreFragments); + return true; + } + + switch (nextHeader) + { + case 0: // Hop-by-Hop Options + case 43: // Routing + case 60: // Destination Options + if (packet.Length < offset + 2) + return false; + nextHeader = packet[offset]; + int extensionLength = checked((packet[offset + 1] + 1) * 8); + if (extensionLength < 8 || packet.Length < offset + extensionLength) + return false; + offset += extensionLength; + break; + + case 51: // Authentication Header + if (packet.Length < offset + 2) + return false; + nextHeader = packet[offset]; + int authenticationLength = checked((packet[offset + 1] + 2) * 4); + if (authenticationLength < 8 || packet.Length < offset + authenticationLength) + return false; + offset += authenticationLength; + break; + + default: + return false; + } + } + + return false; + } + + + private static bool IsTrackedIpv6FragmentProtocol(byte nextHeader) + { + // The Fragment header can point directly to TCP/UDP or to another extension + // header in the fragmentable part (for example Destination Options -> UDP). + // Keep the immediate Next Header value in the fragment key because every + // fragment carries the same Fragment header, including continuations that no + // longer contain the later extension/transport headers. + return nextHeader is 6 or 17 or 0 or 43 or 51 or 60; + } + + private void MaybeSweep(DateTime nowUtc) + { + if (nowUtc - _lastSweepUtc < TimeSpan.FromSeconds(5)) + return; + + _lastSweepUtc = nowUtc; + foreach ((IpFragmentKey key, Entry entry) in _entries.ToArray()) + { + if (nowUtc - entry.LastSeenUtc > EntryLifetime) + { + _entries.Remove(key); + } + } + } + + private readonly record struct Entry(FragmentDisposition Disposition, DateTime LastSeenUtc); +} diff --git a/Shadowsocks.NetworkService/Routing/NetworkClassifier.cs b/Shadowsocks.NetworkService/Routing/NetworkClassifier.cs new file mode 100644 index 000000000..c0671e464 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/NetworkClassifier.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Sockets; + +namespace Shadowsocks.NetworkService.Routing; + +internal static class NetworkClassifier +{ + public static bool IsInfrastructureTraffic(FlowKey flow) + { + if (flow.Protocol is not (6 or 17)) + { + return true; + } + + // Port 53 is handled before this classifier by the explicit DNS policy. + // Keeping it out of the generic infrastructure rules prevents an enabled + // DNSCrypt policy from being accidentally bypassed. + + // DHCP, mDNS and LLMNR are local discovery/bootstrap traffic and must never + // be sent to a remote Shadowsocks server. + if (flow.Protocol == 17 && flow.RemotePort is 67 or 68 or 5353 or 5355) + { + return true; + } + + IPAddress address = flow.RemoteAddress; + if (IPAddress.IsLoopback(address) || address.IsIPv6LinkLocal || address.IsIPv6Multicast) + { + return true; + } + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + byte[] bytes = address.GetAddressBytes(); + if (bytes[0] == 10 + || bytes[0] == 127 + || (bytes[0] == 169 && bytes[1] == 254) + || (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) + || (bytes[0] == 192 && bytes[1] == 168) + || (bytes[0] >= 224)) + { + return true; + } + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + byte[] bytes = address.GetAddressBytes(); + // fc00::/7 unique-local range. + if ((bytes[0] & 0xFE) == 0xFC) + { + return true; + } + } + + return false; + } +} diff --git a/Shadowsocks.NetworkService/Routing/ProcessAttributionCache.cs b/Shadowsocks.NetworkService/Routing/ProcessAttributionCache.cs new file mode 100644 index 000000000..f603179f0 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/ProcessAttributionCache.cs @@ -0,0 +1,120 @@ +#nullable enable +using System.Buffers.Binary; +using System.Collections.Concurrent; + +namespace Shadowsocks.NetworkService.Routing; + +internal static class ProcessAttributionCache +{ + private static readonly TimeSpan EntryLifetime = TimeSpan.FromMinutes(5); + private static readonly ConcurrentDictionary> Entries = new(); + private static long _lastSweepTicks; + + public static void Observe(byte protocol, ushort observedPort, int processId, DateTime nowUtc) + { + if (protocol is not 6 and not 17 || observedPort == 0 || processId <= 0) + return; + + ObserveNormalized(protocol, observedPort, processId, nowUtc); + ushort reversed = BinaryPrimitives.ReverseEndianness(observedPort); + if (reversed != observedPort) + { + // WinDivert exposes the flow 5-tuple, but keeping both byte-order forms here + // makes the cache robust across native representation details. Ambiguous ports + // are deliberately rejected by TryGetProcessId rather than guessed. + ObserveNormalized(protocol, reversed, processId, nowUtc); + } + MaybeSweep(nowUtc); + } + + public static void Remove(byte protocol, ushort observedPort, int processId) + { + RemoveNormalized(protocol, observedPort, processId); + ushort reversed = BinaryPrimitives.ReverseEndianness(observedPort); + if (reversed != observedPort) + RemoveNormalized(protocol, reversed, processId); + } + + public static bool TryGetProcessId(byte protocol, ushort localPort, DateTime nowUtc, out int processId) + { + MaybeSweep(nowUtc); + LocalEndpointKey key = new(protocol, localPort); + if (!Entries.TryGetValue(key, out ConcurrentDictionary? candidates)) + { + processId = 0; + return false; + } + + int resolved = 0; + foreach ((int candidate, DateTime lastSeenUtc) in candidates) + { + if (nowUtc - lastSeenUtc > EntryLifetime) + continue; + if (resolved != 0 && resolved != candidate) + { + processId = 0; + return false; + } + resolved = candidate; + } + + processId = resolved; + return processId > 0; + } + + internal static void ClearForTests() + { + Entries.Clear(); + Interlocked.Exchange(ref _lastSweepTicks, 0); + } + + private static void ObserveNormalized(byte protocol, ushort localPort, int processId, DateTime nowUtc) + { + LocalEndpointKey key = new(protocol, localPort); + ConcurrentDictionary candidates = Entries.GetOrAdd( + key, + static _ => new ConcurrentDictionary()); + candidates[processId] = nowUtc; + } + + private static void RemoveNormalized(byte protocol, ushort localPort, int processId) + { + LocalEndpointKey key = new(protocol, localPort); + if (!Entries.TryGetValue(key, out ConcurrentDictionary? candidates)) + return; + + candidates.TryRemove(processId, out _); + if (candidates.IsEmpty) + { + Entries.TryRemove( + new KeyValuePair>(key, candidates)); + } + } + + private static void MaybeSweep(DateTime nowUtc) + { + long ticks = nowUtc.Ticks; + long previous = Interlocked.Read(ref _lastSweepTicks); + if (ticks - previous < TimeSpan.FromMinutes(1).Ticks + || Interlocked.CompareExchange(ref _lastSweepTicks, ticks, previous) != previous) + { + return; + } + + foreach ((LocalEndpointKey key, ConcurrentDictionary candidates) in Entries) + { + foreach ((int processId, DateTime lastSeenUtc) in candidates) + { + if (nowUtc - lastSeenUtc > EntryLifetime) + candidates.TryRemove(new KeyValuePair(processId, lastSeenUtc)); + } + if (candidates.IsEmpty) + { + Entries.TryRemove( + new KeyValuePair>(key, candidates)); + } + } + } + + private readonly record struct LocalEndpointKey(byte Protocol, ushort LocalPort); +} diff --git a/Shadowsocks.NetworkService/Routing/ProcessResolver.cs b/Shadowsocks.NetworkService/Routing/ProcessResolver.cs new file mode 100644 index 000000000..4b6835ec5 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/ProcessResolver.cs @@ -0,0 +1,382 @@ +using System.Buffers.Binary; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Shadowsocks.NetworkService.Routing; + +internal readonly record struct ProcessIdentity(int ProcessId, string? ProcessPath, string? ProcessName) +{ + public static ProcessIdentity Unknown => new(0, null, null); +} + +internal static partial class ProcessResolver +{ + private const int AfInet = 2; + private const int AfInet6 = 23; + private const uint ErrorInsufficientBuffer = 122; + private const int DnsOwnerLookupAttempts = 4; + + public static ProcessIdentity Resolve(FlowKey flow) + { + int pid; + if (!ProcessAttributionCache.TryGetProcessId(flow.Protocol, flow.LocalPort, DateTime.UtcNow, out pid)) + { + pid = ResolvePidWithRetry( + () => FindOwnerPid(flow), + flow.RemotePort == 53, + static delayMilliseconds => Thread.Sleep(delayMilliseconds)); + } + + if (pid <= 0) + { + return ProcessIdentity.Unknown; + } + + try + { + using Process process = Process.GetProcessById(pid); + string? path = null; + try + { + path = process.MainModule?.FileName; + } + catch + { + } + + string? name = null; + try + { + name = process.ProcessName; + } + catch + { + } + + return new ProcessIdentity(pid, path, name); + } + catch + { + return new ProcessIdentity(pid, null, null); + } + } + + private static int FindOwnerPid(FlowKey flow) + { + return flow.Protocol switch + { + 6 => flow.LocalAddress.AddressFamily == AddressFamily.InterNetwork + ? FindTcp4(flow) + : FindTcp6(flow), + 17 => flow.LocalAddress.AddressFamily == AddressFamily.InterNetwork + ? FindUdp4(flow) + : FindUdp6(flow), + _ => 0, + }; + } + + internal static int ResolvePidWithRetry( + Func resolvePid, + bool dnsFlow, + Action? delay = null) + { + ArgumentNullException.ThrowIfNull(resolvePid); + delay ??= static milliseconds => Thread.Sleep(milliseconds); + + int attempts = dnsFlow ? DnsOwnerLookupAttempts : 1; + for (int attempt = 0; attempt < attempts; attempt++) + { + int processId = resolvePid(); + if (processId > 0) + { + return processId; + } + + if (attempt + 1 < attempts) + { + // DNSCrypt's short-lived -check process and a newly restarted runtime can emit + // bootstrap DNS immediately after creating their socket. Give the Windows owner + // tables a few milliseconds to expose the PID before applying port-53 policy. + delay(attempt switch { 0 => 1, 1 => 4, _ => 10 }); + } + } + + return 0; + } + + private static int FindTcp4(FlowKey flow) + { + nint buffer = 0; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedTcpTable(0, ref size, false, AfInet, TcpTableClass.OwnerPidAll, 0); + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedTcpTable(buffer, ref size, false, AfInet, TcpTableClass.OwnerPidAll, 0); + if (result != 0) + { + return 0; + } + + uint localAddr = ToIpv4UInt32(flow.LocalAddress); + uint remoteAddr = ToIpv4UInt32(flow.RemoteAddress); + int count = Marshal.ReadInt32(buffer); + nint rowPtr = buffer + sizeof(uint); + int rowSize = Marshal.SizeOf(); + for (int i = 0; i < count; i++, rowPtr += rowSize) + { + MibTcpRowOwnerPid row = Marshal.PtrToStructure(rowPtr); + if (row.LocalAddr == localAddr + && DecodePort(row.LocalPort) == flow.LocalPort + && row.RemoteAddr == remoteAddr + && DecodePort(row.RemotePort) == flow.RemotePort) + { + return unchecked((int)row.OwningPid); + } + } + } + finally + { + if (buffer != 0) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static int FindTcp6(FlowKey flow) + { + nint buffer = 0; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedTcpTable(0, ref size, false, AfInet6, TcpTableClass.OwnerPidAll, 0); + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedTcpTable(buffer, ref size, false, AfInet6, TcpTableClass.OwnerPidAll, 0); + if (result != 0) + { + return 0; + } + + byte[] local = flow.LocalAddress.GetAddressBytes(); + byte[] remote = flow.RemoteAddress.GetAddressBytes(); + int count = Marshal.ReadInt32(buffer); + nint rowPtr = buffer + sizeof(uint); + int rowSize = Marshal.SizeOf(); + for (int i = 0; i < count; i++, rowPtr += rowSize) + { + MibTcp6RowOwnerPid row = Marshal.PtrToStructure(rowPtr); + if (row.LocalAddr.AsSpan().SequenceEqual(local) + && DecodePort(row.LocalPort) == flow.LocalPort + && row.RemoteAddr.AsSpan().SequenceEqual(remote) + && DecodePort(row.RemotePort) == flow.RemotePort) + { + return unchecked((int)row.OwningPid); + } + } + } + finally + { + if (buffer != 0) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static int FindUdp4(FlowKey flow) + { + nint buffer = 0; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedUdpTable(0, ref size, false, AfInet, UdpTableClass.OwnerPid, 0); + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedUdpTable(buffer, ref size, false, AfInet, UdpTableClass.OwnerPid, 0); + if (result != 0) + { + return 0; + } + + uint localAddr = ToIpv4UInt32(flow.LocalAddress); + int count = Marshal.ReadInt32(buffer); + nint rowPtr = buffer + sizeof(uint); + int rowSize = Marshal.SizeOf(); + for (int i = 0; i < count; i++, rowPtr += rowSize) + { + MibUdpRowOwnerPid row = Marshal.PtrToStructure(rowPtr); + if ((row.LocalAddr == 0 || row.LocalAddr == localAddr) + && DecodePort(row.LocalPort) == flow.LocalPort) + { + return unchecked((int)row.OwningPid); + } + } + } + finally + { + if (buffer != 0) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static int FindUdp6(FlowKey flow) + { + nint buffer = 0; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedUdpTable(0, ref size, false, AfInet6, UdpTableClass.OwnerPid, 0); + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedUdpTable(buffer, ref size, false, AfInet6, UdpTableClass.OwnerPid, 0); + if (result != 0) + { + return 0; + } + + byte[] local = flow.LocalAddress.GetAddressBytes(); + int count = Marshal.ReadInt32(buffer); + nint rowPtr = buffer + sizeof(uint); + int rowSize = Marshal.SizeOf(); + for (int i = 0; i < count; i++, rowPtr += rowSize) + { + MibUdp6RowOwnerPid row = Marshal.PtrToStructure(rowPtr); + bool any = row.LocalAddr.All(static b => b == 0); + if ((any || row.LocalAddr.AsSpan().SequenceEqual(local)) + && DecodePort(row.LocalPort) == flow.LocalPort) + { + return unchecked((int)row.OwningPid); + } + } + } + finally + { + if (buffer != 0) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static uint ToIpv4UInt32(IPAddress address) + => BinaryPrimitives.ReadUInt32LittleEndian(address.MapToIPv4().GetAddressBytes()); + + private static ushort DecodePort(uint encoded) + { + ushort networkOrder = unchecked((ushort)encoded); + return unchecked((ushort)IPAddress.NetworkToHostOrder(unchecked((short)networkOrder))); + } + + private enum TcpTableClass + { + BasicListener, + BasicConnections, + BasicAll, + OwnerPidListener, + OwnerPidConnections, + OwnerPidAll, + OwnerModuleListener, + OwnerModuleConnections, + OwnerModuleAll, + } + + private enum UdpTableClass + { + Basic, + OwnerPid, + OwnerModule, + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibTcpRowOwnerPid + { + public uint State; + public uint LocalAddr; + public uint LocalPort; + public uint RemoteAddr; + public uint RemotePort; + public uint OwningPid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibTcp6RowOwnerPid + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public byte[] LocalAddr; + public uint LocalScopeId; + public uint LocalPort; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public byte[] RemoteAddr; + public uint RemoteScopeId; + public uint RemotePort; + public uint State; + public uint OwningPid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibUdpRowOwnerPid + { + public uint LocalAddr; + public uint LocalPort; + public uint OwningPid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibUdp6RowOwnerPid + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public byte[] LocalAddr; + public uint LocalScopeId; + public uint LocalPort; + public uint OwningPid; + } + + private static partial class NativeMethods + { + [LibraryImport("iphlpapi.dll", SetLastError = true)] + internal static partial uint GetExtendedTcpTable( + nint table, + ref int size, + [MarshalAs(UnmanagedType.Bool)] bool order, + int addressFamily, + TcpTableClass tableClass, + uint reserved); + + [LibraryImport("iphlpapi.dll", SetLastError = true)] + internal static partial uint GetExtendedUdpTable( + nint table, + ref int size, + [MarshalAs(UnmanagedType.Bool)] bool order, + int addressFamily, + UdpTableClass tableClass, + uint reserved); + } +} diff --git a/Shadowsocks.NetworkService/Routing/Socks5Connector.cs b/Shadowsocks.NetworkService/Routing/Socks5Connector.cs new file mode 100644 index 000000000..6af6a6ea9 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/Socks5Connector.cs @@ -0,0 +1,142 @@ +using System.Buffers.Binary; +using System.Net; +using System.Net.Sockets; + +namespace Shadowsocks.NetworkService.Routing; + +internal static class Socks5Connector +{ + public static async Task ConnectAsync( + string proxyHost, + int proxyPort, + IPAddress destinationAddress, + int destinationPort, + CancellationToken cancellationToken) + { + Socket socket = new(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + await socket.ConnectAsync(proxyHost, proxyPort, cancellationToken).ConfigureAwait(false); + using NetworkStream stream = new(socket, ownsSocket: false); + await stream.WriteAsync(new byte[] { 5, 1, 0 }, cancellationToken).ConfigureAwait(false); + byte[] response = new byte[2]; + await ReadExactlyAsync(stream, response, cancellationToken).ConfigureAwait(false); + if (response[0] != 5 || response[1] != 0) + { + throw new IOException("Local Shadowsocks SOCKS5 listener rejected authentication negotiation."); + } + + byte[] request = BuildConnectRequest(destinationAddress, destinationPort); + await stream.WriteAsync(request, cancellationToken).ConfigureAwait(false); + byte[] header = new byte[4]; + await ReadExactlyAsync(stream, header, cancellationToken).ConfigureAwait(false); + if (header[0] != 5 || header[1] != 0) + { + throw new IOException($"SOCKS5 CONNECT failed with reply 0x{header[1]:X2}."); + } + + int addressLength = header[3] switch + { + 1 => 4, + 4 => 16, + 3 => await ReadDomainLengthAsync(stream, cancellationToken).ConfigureAwait(false), + _ => throw new IOException("Invalid SOCKS5 response address type."), + }; + byte[] discard = new byte[addressLength + 2]; + await ReadExactlyAsync(stream, discard, cancellationToken).ConfigureAwait(false); + return socket; + } + catch + { + socket.Dispose(); + throw; + } + } + + public static byte[] BuildUdpDatagram(IPEndPoint destination, ReadOnlySpan payload) + { + byte[] address = destination.Address.GetAddressBytes(); + int headerLength = 3 + 1 + address.Length + 2; + byte[] packet = new byte[headerLength + payload.Length]; + packet[0] = 0; + packet[1] = 0; + packet[2] = 0; + packet[3] = destination.AddressFamily == AddressFamily.InterNetwork ? (byte)1 : (byte)4; + address.CopyTo(packet.AsSpan(4)); + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(4 + address.Length, 2), checked((ushort)destination.Port)); + payload.CopyTo(packet.AsSpan(headerLength)); + return packet; + } + + public static bool TryParseUdpDatagram(ReadOnlySpan packet, out IPEndPoint? source, out byte[] payload) + { + source = null; + payload = []; + if (packet.Length < 7 || packet[0] != 0 || packet[1] != 0 || packet[2] != 0) + { + return false; + } + + int offset = 4; + IPAddress address; + switch (packet[3]) + { + case 1: + if (packet.Length < offset + 4 + 2) return false; + address = new IPAddress(packet.Slice(offset, 4)); + offset += 4; + break; + case 4: + if (packet.Length < offset + 16 + 2) return false; + address = new IPAddress(packet.Slice(offset, 16)); + offset += 16; + break; + case 3: + // Shadowsocks UDP responses are normally IP based. DNS names cannot be + // injected as an IP packet until the DNS layer is implemented. + return false; + default: + return false; + } + + ushort port = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(offset, 2)); + offset += 2; + source = new IPEndPoint(address, port); + payload = packet[offset..].ToArray(); + return true; + } + + private static byte[] BuildConnectRequest(IPAddress destinationAddress, int destinationPort) + { + byte[] address = destinationAddress.GetAddressBytes(); + byte[] request = new byte[4 + address.Length + 2]; + request[0] = 5; + request[1] = 1; + request[2] = 0; + request[3] = destinationAddress.AddressFamily == AddressFamily.InterNetwork ? (byte)1 : (byte)4; + address.CopyTo(request.AsSpan(4)); + BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(4 + address.Length, 2), checked((ushort)destinationPort)); + return request; + } + + private static async Task ReadDomainLengthAsync(Stream stream, CancellationToken cancellationToken) + { + byte[] length = new byte[1]; + await ReadExactlyAsync(stream, length, cancellationToken).ConfigureAwait(false); + return length[0]; + } + + private static async Task ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) + { + int offset = 0; + while (offset < buffer.Length) + { + int read = await stream.ReadAsync(buffer[offset..], cancellationToken).ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException(); + } + offset += read; + } + } +} diff --git a/Shadowsocks.NetworkService/Routing/TransparentDnsRelay.cs b/Shadowsocks.NetworkService/Routing/TransparentDnsRelay.cs new file mode 100644 index 000000000..3632e0611 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/TransparentDnsRelay.cs @@ -0,0 +1,791 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; + +namespace Shadowsocks.NetworkService.Routing; + +/// +/// Local transparent DNS bridge. WinDivert reflects application DNS packets into +/// the two listeners below. The bridge forwards the DNS payload/stream directly +/// to the DNSCrypt loopback listener, then emits replies through the same reflected +/// flow mechanism used by the Shadowsocks transparent relays. +/// +internal sealed class TransparentDnsRelay : IAsyncDisposable +{ + private static readonly TimeSpan DefaultUdpIdleTimeout = TimeSpan.FromMinutes(2); + private static readonly TimeSpan DefaultUdpSweepInterval = TimeSpan.FromSeconds(30); + + private static readonly TimeSpan UdpFallbackDelay = TimeSpan.FromMilliseconds(750); + private static readonly TimeSpan UdpPendingLifetime = TimeSpan.FromSeconds(3); + + private readonly FlowRegistry _flows; + private readonly IReadOnlyList _upstreamEndpoints; + private readonly IPEndPoint? _localSocksEndpoint; + private readonly TimeSpan _udpIdleTimeout; + private readonly TimeSpan _udpSweepInterval; + private readonly CancellationTokenSource _shutdown = new(); + private readonly Socket _udpListener; + private readonly Socket _tcpListener; + private readonly ConcurrentDictionary _udpFlows = new(); + private readonly ConcurrentDictionary _tcpConnections = new(); + private Task? _udpLoop; + private Task? _udpSweepLoop; + private Task? _tcpLoop; + private long _nextTcpConnectionId; + private int _started; + private int _disposed; + + public TransparentDnsRelay( + FlowRegistry flows, + int dnsCryptPort, + TimeSpan? udpIdleTimeout = null, + TimeSpan? udpSweepInterval = null) + : this(flows, [new IPEndPoint(IPAddress.Loopback, dnsCryptPort)], udpIdleTimeout, udpSweepInterval) + { + } + + public TransparentDnsRelay( + FlowRegistry flows, + IPEndPoint upstreamEndpoint, + TimeSpan? udpIdleTimeout = null, + TimeSpan? udpSweepInterval = null) + : this(flows, [upstreamEndpoint], udpIdleTimeout, udpSweepInterval) + { + } + + public TransparentDnsRelay( + FlowRegistry flows, + IReadOnlyList upstreamEndpoints, + TimeSpan? udpIdleTimeout = null, + TimeSpan? udpSweepInterval = null, + string? proxyHost = null, + int proxyPort = 0) + { + ArgumentNullException.ThrowIfNull(upstreamEndpoints); + IPEndPoint[] normalizedEndpoints = upstreamEndpoints + .Where(endpoint => endpoint is not null) + .DistinctBy(endpoint => $"{endpoint.Address}|{endpoint.Port}", StringComparer.OrdinalIgnoreCase) + .ToArray(); + if (normalizedEndpoints.Length == 0) + throw new ArgumentException("At least one DNS upstream endpoint is required.", nameof(upstreamEndpoints)); + if (normalizedEndpoints.Any(endpoint => endpoint.Port is < 1 or > 65535)) + throw new ArgumentOutOfRangeException(nameof(upstreamEndpoints)); + + ArgumentNullException.ThrowIfNull(flows); + _flows = flows; + _upstreamEndpoints = normalizedEndpoints; + if (!string.IsNullOrWhiteSpace(proxyHost)) + { + if (proxyPort is < 1 or > 65535) + throw new ArgumentOutOfRangeException(nameof(proxyPort)); + IPAddress proxyAddress = IPAddress.TryParse(proxyHost, out IPAddress? parsed) + ? parsed + : Dns.GetHostAddresses(proxyHost).First(static ip => ip.AddressFamily == AddressFamily.InterNetwork); + _localSocksEndpoint = new IPEndPoint(proxyAddress, proxyPort); + } + _udpIdleTimeout = udpIdleTimeout ?? DefaultUdpIdleTimeout; + _udpSweepInterval = udpSweepInterval ?? DefaultUdpSweepInterval; + if (_udpIdleTimeout <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(udpIdleTimeout)); + } + if (_udpSweepInterval <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(udpSweepInterval)); + } + + _udpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) + { + DualMode = true, + }; + _udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _udpListener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + UdpPort = ((IPEndPoint)_udpListener.LocalEndPoint!).Port; + + _tcpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) + { + DualMode = true, + NoDelay = true, + }; + _tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _tcpListener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + _tcpListener.Listen(256); + TcpPort = ((IPEndPoint)_tcpListener.LocalEndPoint!).Port; + } + + public int TcpPort { get; } + public int UdpPort { get; } + internal int UdpFlowCount => _udpFlows.Count; + + public void Start() + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + if (Interlocked.CompareExchange(ref _started, 1, 0) != 0) + { + throw new InvalidOperationException("Transparent DNS relay has already been started."); + } + + _udpLoop = UdpReceiveLoopAsync(); + _udpSweepLoop = UdpSweepLoopAsync(); + _tcpLoop = TcpAcceptLoopAsync(); + } + + private async Task UdpReceiveLoopAsync() + { + byte[] buffer = new byte[65535]; + EndPoint peerTemplate = new IPEndPoint(IPAddress.IPv6Any, 0); + while (!_shutdown.IsCancellationRequested) + { + try + { + SocketReceiveFromResult result = await _udpListener + .ReceiveFromAsync(buffer, SocketFlags.None, peerTemplate, _shutdown.Token) + .ConfigureAwait(false); + if (result.RemoteEndPoint is not IPEndPoint peer) + { + continue; + } + + IPAddress remoteAddress = Normalize(peer.Address); + if (!_flows.TryGetByPeer(17, remoteAddress, checked((ushort)peer.Port), out FlowState? state) + || state is null + || state.Key.RemotePort != 53) + { + continue; + } + + UdpDnsFlow flow = _udpFlows.GetOrAdd( + state.Key, + key => new UdpDnsFlow( + key, + _udpListener, + _upstreamEndpoints, + _localSocksEndpoint, + RemoveFaultedUdpFlow, + _shutdown.Token)); + try + { + await flow.SendAsync(buffer.AsMemory(0, result.ReceivedBytes)).ConfigureAwait(false); + } + catch (SocketException) + { + RemoveUdpFlow(state.Key, flow); + } + catch (ObjectDisposedException) + { + RemoveUdpFlow(state.Key, flow); + } + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_shutdown.IsCancellationRequested) + { + break; + } + catch (SocketException) + { + // A single malformed/broken UDP flow must never terminate the listener. + try + { + await Task.Delay(25, _shutdown.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + } + } + } + + private async Task UdpSweepLoopAsync() + { + while (!_shutdown.IsCancellationRequested) + { + try + { + await Task.Delay(_udpSweepInterval, _shutdown.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + + DateTime now = DateTime.UtcNow; + foreach ((FlowKey key, UdpDnsFlow flow) in _udpFlows) + { + if (now - flow.LastSeenUtc > _udpIdleTimeout) + { + RemoveUdpFlow(key, flow); + } + } + } + } + + private void RemoveFaultedUdpFlow(FlowKey key, UdpDnsFlow flow) + => RemoveUdpFlow(key, flow); + + private void RemoveUdpFlow(FlowKey key, UdpDnsFlow flow) + { + if (_udpFlows.TryRemove(new KeyValuePair(key, flow))) + { + _ = DisposeUdpFlowAsync(flow); + } + } + + private static async Task DisposeUdpFlowAsync(UdpDnsFlow flow) + { + try + { + await flow.DisposeAsync().ConfigureAwait(false); + } + catch + { + } + } + + private async Task TcpAcceptLoopAsync() + { + while (!_shutdown.IsCancellationRequested) + { + Socket client; + try + { + client = await _tcpListener.AcceptAsync(_shutdown.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_shutdown.IsCancellationRequested) + { + break; + } + catch (SocketException) + { + continue; + } + + TrackTcpConnection(client); + } + } + + + private void TrackTcpConnection(Socket client) + { + long id = Interlocked.Increment(ref _nextTcpConnectionId); + Task task = HandleTcpConnectionAsync(client); + _tcpConnections[id] = task; + + if (task.IsCompleted) + { + _tcpConnections.TryRemove(id, out _); + return; + } + + _ = task.ContinueWith( + completed => + { + _tcpConnections.TryRemove(id, out _); + _ = completed.Exception; + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + private async Task HandleTcpConnectionAsync(Socket client) + { + using (client) + { + client.NoDelay = true; + if (client.RemoteEndPoint is not IPEndPoint peer || client.LocalEndPoint is not IPEndPoint local) + { + return; + } + + IPAddress remoteAddress = Normalize(peer.Address); + IPAddress localAddress = Normalize(local.Address); + if (!_flows.TryGetReflected(6, localAddress, checked((ushort)peer.Port), remoteAddress, out FlowState? state) + || state is null + || state.Key.RemotePort != 53) + { + return; + } + + using Socket? upstream = await ConnectTcpUpstreamAsync(_shutdown.Token).ConfigureAwait(false); + if (upstream is null) + return; + + using NetworkStream clientStream = new(client, ownsSocket: false); + using NetworkStream upstreamStream = new(upstream, ownsSocket: false); + Task clientToUpstream = PumpAsync(clientStream, upstreamStream, _shutdown.Token); + Task upstreamToClient = PumpAsync(upstreamStream, clientStream, _shutdown.Token); + await Task.WhenAny(clientToUpstream, upstreamToClient).ConfigureAwait(false); + + TryShutdown(client); + TryShutdown(upstream); + await Task.WhenAll(clientToUpstream, upstreamToClient).ConfigureAwait(false); + } + } + + + private async Task ConnectTcpUpstreamAsync(CancellationToken cancellationToken) + { + foreach (IPEndPoint endpoint in _upstreamEndpoints) + { + Socket? socket = null; + try + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(2)); + if (_localSocksEndpoint is not null) + { + socket = await Socks5Connector.ConnectAsync( + _localSocksEndpoint.Address.ToString(), + _localSocksEndpoint.Port, + endpoint.Address, + endpoint.Port, + timeout.Token).ConfigureAwait(false); + } + else + { + socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + await socket.ConnectAsync(endpoint, timeout.Token).ConfigureAwait(false); + } + return socket; + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + socket?.Dispose(); + } + catch (SocketException) + { + socket?.Dispose(); + } + catch (IOException) + { + socket?.Dispose(); + } + } + return null; + } + + + private static void TryShutdown(Socket socket) + { + try + { + socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) + { + } + catch (ObjectDisposedException) + { + } + } + + private static async Task PumpAsync(Stream source, Stream destination, CancellationToken token) + { + byte[] buffer = new byte[32 * 1024]; + try + { + while (true) + { + int read = await source.ReadAsync(buffer, token).ConfigureAwait(false); + if (read == 0) + { + return; + } + + await destination.WriteAsync(buffer.AsMemory(0, read), token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (IOException) + { + } + catch (SocketException) + { + } + catch (ObjectDisposedException) + { + } + } + + private static IPAddress Normalize(IPAddress address) + => address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address; + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + _shutdown.Cancel(); + _udpListener.Dispose(); + _tcpListener.Dispose(); + + UdpDnsFlow[] udpFlows = _udpFlows.Values.ToArray(); + _udpFlows.Clear(); + foreach (UdpDnsFlow flow in udpFlows) + { + flow.Dispose(); + } + + await AwaitIgnoringShutdownAsync(_udpLoop).ConfigureAwait(false); + await AwaitIgnoringShutdownAsync(_udpSweepLoop).ConfigureAwait(false); + await AwaitIgnoringShutdownAsync(_tcpLoop).ConfigureAwait(false); + + Task[] tcpConnections = _tcpConnections.Values.ToArray(); + if (tcpConnections.Length > 0) + { + try + { + await Task.WhenAll(tcpConnections).ConfigureAwait(false); + } + catch + { + } + } + _tcpConnections.Clear(); + + foreach (UdpDnsFlow flow in udpFlows) + { + await flow.DisposeAsync().ConfigureAwait(false); + } + + _shutdown.Dispose(); + } + + private static async Task AwaitIgnoringShutdownAsync(Task? task) + { + if (task is null) + { + return; + } + + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + } + } + + internal static IPAddress SelectUdpBindAddress(IPAddress upstreamAddress) + { + ArgumentNullException.ThrowIfNull(upstreamAddress); + if (IPAddress.IsLoopback(upstreamAddress)) + return upstreamAddress.AddressFamily == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Loopback + : IPAddress.Loopback; + return upstreamAddress.AddressFamily == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Any + : IPAddress.Any; + } + + private sealed class UdpDnsFlow : IDisposable, IAsyncDisposable + { + private sealed class PendingQuery + { + public PendingQuery(long generation, byte[] payload) + { + Generation = generation; + Payload = payload; + } + + public long Generation { get; } + public byte[] Payload { get; } + public int Completed; + } + + private readonly FlowKey _flow; + private readonly Socket _transparentListener; + private readonly CancellationToken _token; + private readonly Action _onFault; + private readonly List _dnsSockets; + private readonly IReadOnlyList _upstreamEndpoints; + private readonly IPEndPoint? _localSocksEndpoint; + private readonly SemaphoreSlim _sendLock = new(1, 1); + private readonly ConcurrentDictionary _pending = new(); + private readonly Task[] _receiveLoops; + private long _lastSeenTicks; + private long _generation; + private int _disposed; + private int _cleanupStarted; + + public UdpDnsFlow( + FlowKey flow, + Socket transparentListener, + IReadOnlyList upstreamEndpoints, + IPEndPoint? localSocksEndpoint, + Action onFault, + CancellationToken token) + { + _flow = flow; + _transparentListener = transparentListener; + _token = token; + _onFault = onFault; + _upstreamEndpoints = upstreamEndpoints; + _localSocksEndpoint = localSocksEndpoint; + _lastSeenTicks = DateTime.UtcNow.Ticks; + + var sockets = new List(upstreamEndpoints.Count); + try + { + foreach (IPEndPoint endpoint in upstreamEndpoints) + { + AddressFamily family = localSocksEndpoint?.AddressFamily ?? endpoint.AddressFamily; + var socket = new Socket(family, SocketType.Dgram, ProtocolType.Udp); + if (localSocksEndpoint is null) + { + IPAddress bindAddress = SelectUdpBindAddress(endpoint.Address); + socket.Bind(new IPEndPoint(bindAddress, 0)); + socket.Connect(endpoint); + } + else + { + IPAddress bindAddress = family == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Loopback + : IPAddress.Loopback; + socket.Bind(new IPEndPoint(bindAddress, 0)); + } + sockets.Add(socket); + } + } + catch + { + foreach (Socket socket in sockets) + socket.Dispose(); + throw; + } + + _dnsSockets = sockets; + _receiveLoops = sockets.Select((socket, index) => ReceiveFromUpstreamAsync(socket, index)).ToArray(); + } + + public DateTime LastSeenUtc => new(Interlocked.Read(ref _lastSeenTicks), DateTimeKind.Utc); + + public async Task SendAsync(ReadOnlyMemory payload) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + if (payload.Length < 2) + return; + + Touch(); + ushort transactionId = checked((ushort)((payload.Span[0] << 8) | payload.Span[1])); + var pending = new PendingQuery(Interlocked.Increment(ref _generation), payload.ToArray()); + _pending[transactionId] = pending; + + await _sendLock.WaitAsync(_token).ConfigureAwait(false); + try + { + await SendToUpstreamAsync(0, payload).ConfigureAwait(false); + Touch(); + } + finally + { + _sendLock.Release(); + } + + _ = ScheduleFallbackAndCleanupAsync(transactionId, pending); + } + + private async Task ScheduleFallbackAndCleanupAsync(ushort transactionId, PendingQuery pending) + { + try + { + if (_dnsSockets.Count > 1) + { + await Task.Delay(UdpFallbackDelay, _token).ConfigureAwait(false); + if (!IsPending(transactionId, pending)) + return; + + await _sendLock.WaitAsync(_token).ConfigureAwait(false); + try + { + // A backup is a fallback, not a fan-out: it receives the query only + // after the primary had a chance to answer first. + await SendToUpstreamAsync(1, pending.Payload).ConfigureAwait(false); + Touch(); + } + finally + { + _sendLock.Release(); + } + } + + TimeSpan remaining = UdpPendingLifetime - (_dnsSockets.Count > 1 ? UdpFallbackDelay : TimeSpan.Zero); + if (remaining > TimeSpan.Zero) + await Task.Delay(remaining, _token).ConfigureAwait(false); + if (IsPending(transactionId, pending)) + _pending.TryRemove(new KeyValuePair(transactionId, pending)); + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + if (_dnsSockets.Count == 1) + _onFault(_flow, this); + } + } + + private async Task SendToUpstreamAsync(int index, ReadOnlyMemory payload) + { + Socket socket = _dnsSockets[index]; + if (_localSocksEndpoint is null) + { + await socket.SendAsync(payload, SocketFlags.None, _token).ConfigureAwait(false); + return; + } + + byte[] packet = Socks5Connector.BuildUdpDatagram(_upstreamEndpoints[index], payload.Span); + await socket.SendToAsync(packet, SocketFlags.None, _localSocksEndpoint, _token).ConfigureAwait(false); + } + + private bool IsPending(ushort transactionId, PendingQuery pending) + => Volatile.Read(ref pending.Completed) == 0 + && _pending.TryGetValue(transactionId, out PendingQuery? current) + && ReferenceEquals(current, pending); + + private async Task ReceiveFromUpstreamAsync(Socket dnsSocket, int upstreamIndex) + { + byte[] buffer = new byte[65535]; + while (!_token.IsCancellationRequested && Volatile.Read(ref _disposed) == 0) + { + try + { + ReadOnlyMemory responsePayload; + if (_localSocksEndpoint is null) + { + int received = await dnsSocket.ReceiveAsync(buffer, SocketFlags.None, _token).ConfigureAwait(false); + responsePayload = buffer.AsMemory(0, received); + } + else + { + EndPoint sender = new IPEndPoint( + _localSocksEndpoint.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, + 0); + SocketReceiveFromResult received = await dnsSocket + .ReceiveFromAsync(buffer, SocketFlags.None, sender, _token) + .ConfigureAwait(false); + if (!Socks5Connector.TryParseUdpDatagram( + buffer.AsSpan(0, received.ReceivedBytes), + out IPEndPoint? source, + out byte[] payload) + || source is null + || !source.Address.Equals(_upstreamEndpoints[upstreamIndex].Address) + || source.Port != _upstreamEndpoints[upstreamIndex].Port) + { + continue; + } + responsePayload = payload; + } + if (responsePayload.Length < 2) + continue; + + ushort transactionId = checked((ushort)((responsePayload.Span[0] << 8) | responsePayload.Span[1])); + if (!_pending.TryGetValue(transactionId, out PendingQuery? pending) + || Interlocked.Exchange(ref pending.Completed, 1) != 0 + || !_pending.TryRemove(new KeyValuePair(transactionId, pending))) + { + continue; + } + + Touch(); + IPEndPoint fakePeer = new(_flow.RemoteAddress, _flow.LocalPort); + await _transparentListener + .SendToAsync(responsePayload, SocketFlags.None, fakePeer, _token) + .ConfigureAwait(false); + Touch(); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_token.IsCancellationRequested || Volatile.Read(ref _disposed) != 0) + { + break; + } + catch (SocketException) + { + // One upstream socket can fail while the backup remains useful. Let the + // pending fallback path try the other resolver before faulting the flow. + if (_dnsSockets.Count == 1) + _onFault(_flow, this); + break; + } + } + } + + private void Touch() + => Interlocked.Exchange(ref _lastSeenTicks, DateTime.UtcNow.Ticks); + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + + foreach (Socket socket in _dnsSockets) + socket.Dispose(); + _pending.Clear(); + } + + public async ValueTask DisposeAsync() + { + Dispose(); + if (Interlocked.Exchange(ref _cleanupStarted, 1) != 0) + return; + + try + { + await Task.WhenAll(_receiveLoops).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + } + finally + { + _sendLock.Dispose(); + } + } + } +} diff --git a/Shadowsocks.NetworkService/Routing/TransparentDohRelay.cs b/Shadowsocks.NetworkService/Routing/TransparentDohRelay.cs new file mode 100644 index 000000000..c4c1d1b6d --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/TransparentDohRelay.cs @@ -0,0 +1,361 @@ +#nullable enable +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Sockets; + +namespace Shadowsocks.NetworkService.Routing; + +/// +/// Transparent DNS-over-HTTPS bridge used by Admin Mode. WinDivert reflects +/// application UDP/TCP DNS requests into local listeners; each DNS wire message +/// is sent to the configured HTTPS endpoint and reflected back to the original +/// application flow. +/// +internal sealed class TransparentDohRelay : IAsyncDisposable +{ + private static readonly TimeSpan QueryTimeout = TimeSpan.FromSeconds(10); + + private readonly FlowRegistry _flows; + private readonly Uri _endpoint; + private readonly CancellationTokenSource _shutdown = new(); + private readonly Socket _udpListener; + private readonly Socket _tcpListener; + private readonly HttpClient _httpClient; + private readonly ConcurrentDictionary _operations = new(); + private Task? _udpLoop; + private Task? _tcpLoop; + private long _nextOperationId; + private int _started; + private int _disposed; + + public TransparentDohRelay( + FlowRegistry flows, + string endpoint, + bool routeThroughShadowsocks = false, + string localProxyHost = "127.0.0.1", + int localProxyPort = 1080) + { + ArgumentNullException.ThrowIfNull(flows); + _flows = flows; + if (!Uri.TryCreate(endpoint?.Trim(), UriKind.Absolute, out Uri? uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("Custom DoH endpoint must be an absolute HTTPS URL.", nameof(endpoint)); + } + _endpoint = uri; + + if (routeThroughShadowsocks && (localProxyPort is < 1 or > 65535)) + throw new ArgumentOutOfRangeException(nameof(localProxyPort)); + string normalizedProxyHost = string.IsNullOrWhiteSpace(localProxyHost) + ? "127.0.0.1" + : localProxyHost.Trim().TrimStart('[').TrimEnd(']'); + string proxyHostForUri = normalizedProxyHost.Contains(':', StringComparison.Ordinal) + ? $"[{normalizedProxyHost}]" + : normalizedProxyHost; + + _httpClient = new HttpClient(new SocketsHttpHandler + { + UseProxy = routeThroughShadowsocks, + Proxy = routeThroughShadowsocks + ? new WebProxy($"socks5://{proxyHostForUri}:{localProxyPort}") + : null, + AutomaticDecompression = DecompressionMethods.All, + AllowAutoRedirect = false, + ConnectTimeout = TimeSpan.FromSeconds(5), + PooledConnectionLifetime = TimeSpan.FromMinutes(10), + }) + { + Timeout = Timeout.InfiniteTimeSpan, + }; + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/dns-message")); + + _udpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) + { + DualMode = true, + }; + _udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _udpListener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + UdpPort = ((IPEndPoint)_udpListener.LocalEndPoint!).Port; + + _tcpListener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) + { + DualMode = true, + NoDelay = true, + }; + _tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _tcpListener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + _tcpListener.Listen(256); + TcpPort = ((IPEndPoint)_tcpListener.LocalEndPoint!).Port; + } + + public int TcpPort { get; } + public int UdpPort { get; } + + public void Start() + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + if (Interlocked.CompareExchange(ref _started, 1, 0) != 0) + throw new InvalidOperationException("Transparent DoH relay has already been started."); + + _udpLoop = UdpReceiveLoopAsync(); + _tcpLoop = TcpAcceptLoopAsync(); + } + + private async Task UdpReceiveLoopAsync() + { + byte[] buffer = new byte[65535]; + EndPoint peerTemplate = new IPEndPoint(IPAddress.IPv6Any, 0); + while (!_shutdown.IsCancellationRequested) + { + try + { + SocketReceiveFromResult result = await _udpListener + .ReceiveFromAsync(buffer, SocketFlags.None, peerTemplate, _shutdown.Token) + .ConfigureAwait(false); + if (result.RemoteEndPoint is not IPEndPoint peer) + continue; + + IPAddress remoteAddress = Normalize(peer.Address); + if (!_flows.TryGetByPeer(17, remoteAddress, checked((ushort)peer.Port), out FlowState? state) + || state is null + || state.Key.RemotePort != 53) + { + continue; + } + + byte[] query = buffer.AsSpan(0, result.ReceivedBytes).ToArray(); + TrackOperation(HandleUdpQueryAsync(state, query)); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_shutdown.IsCancellationRequested) + { + break; + } + catch (SocketException) + { + try { await Task.Delay(25, _shutdown.Token).ConfigureAwait(false); } + catch (OperationCanceledException) { break; } + } + } + } + + private async Task HandleUdpQueryAsync(FlowState state, byte[] query) + { + try + { + byte[] response = await QueryDohAsync(query, _shutdown.Token).ConfigureAwait(false); + IPEndPoint fakePeer = new(state.Key.RemoteAddress, state.Key.LocalPort); + await _udpListener + .SendToAsync(response, SocketFlags.None, fakePeer, _shutdown.Token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (HttpRequestException) + { + // Per-query upstream failure: fail closed by not emitting a plaintext reply. + } + catch (SocketException) + { + } + catch (InvalidDataException) + { + } + } + + private async Task TcpAcceptLoopAsync() + { + while (!_shutdown.IsCancellationRequested) + { + try + { + Socket client = await _tcpListener.AcceptAsync(_shutdown.Token).ConfigureAwait(false); + TrackOperation(HandleTcpConnectionAsync(client)); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_shutdown.IsCancellationRequested) + { + break; + } + catch (SocketException) + { + } + } + } + + private async Task HandleTcpConnectionAsync(Socket client) + { + using (client) + { + client.NoDelay = true; + if (client.RemoteEndPoint is not IPEndPoint peer || client.LocalEndPoint is not IPEndPoint local) + return; + + IPAddress remoteAddress = Normalize(peer.Address); + IPAddress localAddress = Normalize(local.Address); + if (!_flows.TryGetReflected(6, localAddress, checked((ushort)peer.Port), remoteAddress, out FlowState? state) + || state is null + || state.Key.RemotePort != 53) + { + return; + } + + using var stream = new NetworkStream(client, ownsSocket: false); + byte[] lengthPrefix = new byte[2]; + while (!_shutdown.IsCancellationRequested) + { + if (!await ReadExactAsync(stream, lengthPrefix, _shutdown.Token).ConfigureAwait(false)) + return; + + int length = BinaryPrimitives.ReadUInt16BigEndian(lengthPrefix); + if (length is < 12 or > 65535) + return; + + byte[] query = new byte[length]; + if (!await ReadExactAsync(stream, query, _shutdown.Token).ConfigureAwait(false)) + return; + + byte[] response; + try + { + response = await QueryDohAsync(query, _shutdown.Token).ConfigureAwait(false); + } + catch (HttpRequestException) + { + return; + } + catch (InvalidDataException) + { + return; + } + + if (response.Length > ushort.MaxValue) + return; + BinaryPrimitives.WriteUInt16BigEndian(lengthPrefix, checked((ushort)response.Length)); + await stream.WriteAsync(lengthPrefix, _shutdown.Token).ConfigureAwait(false); + await stream.WriteAsync(response, _shutdown.Token).ConfigureAwait(false); + await stream.FlushAsync(_shutdown.Token).ConfigureAwait(false); + } + } + } + + private async Task QueryDohAsync(byte[] query, CancellationToken cancellationToken) + { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(QueryTimeout); + using var request = new HttpRequestMessage(HttpMethod.Post, _endpoint) + { + Content = new ByteArrayContent(query), + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/dns-message"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/dns-message")); + using HttpResponseMessage response = await _httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token) + .ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + string? mediaType = response.Content.Headers.ContentType?.MediaType; + if (!string.Equals(mediaType, "application/dns-message", StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException($"DoH endpoint returned unsupported content type '{mediaType ?? ""}'."); + + byte[] payload = await response.Content.ReadAsByteArrayAsync(timeoutCts.Token).ConfigureAwait(false); + if (payload.Length is < 12 or > 65535) + throw new InvalidDataException("DoH endpoint returned an invalid DNS wire message length."); + return payload; + } + + private static async Task ReadExactAsync(Stream stream, Memory buffer, CancellationToken token) + { + int offset = 0; + while (offset < buffer.Length) + { + int read; + try + { + read = await stream.ReadAsync(buffer[offset..], token).ConfigureAwait(false); + } + catch (IOException) + { + return false; + } + catch (SocketException) + { + return false; + } + if (read == 0) + return false; + offset += read; + } + return true; + } + + private void TrackOperation(Task task) + { + long id = Interlocked.Increment(ref _nextOperationId); + _operations[id] = task; + _ = task.ContinueWith( + completed => + { + _operations.TryRemove(id, out _); + _ = completed.Exception; + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + private static IPAddress Normalize(IPAddress address) + => address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address; + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + + _shutdown.Cancel(); + _udpListener.Dispose(); + _tcpListener.Dispose(); + + await AwaitIgnoringShutdownAsync(_udpLoop).ConfigureAwait(false); + await AwaitIgnoringShutdownAsync(_tcpLoop).ConfigureAwait(false); + + Task[] operations = _operations.Values.ToArray(); + if (operations.Length > 0) + { + try { await Task.WhenAll(operations).ConfigureAwait(false); } + catch { } + } + _operations.Clear(); + _httpClient.Dispose(); + _shutdown.Dispose(); + } + + private static async Task AwaitIgnoringShutdownAsync(Task? task) + { + if (task is null) + return; + try { await task.ConfigureAwait(false); } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + catch (SocketException) { } + } +} diff --git a/Shadowsocks.NetworkService/Routing/TransparentTcpRelay.cs b/Shadowsocks.NetworkService/Routing/TransparentTcpRelay.cs new file mode 100644 index 000000000..62d7a9600 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/TransparentTcpRelay.cs @@ -0,0 +1,286 @@ +using System.Net; +using System.Net.Sockets; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.ManagedRouting; + +namespace Shadowsocks.NetworkService.Routing; + +internal sealed class TransparentTcpRelay : IAsyncDisposable +{ + private const int MaxInspectionBytes = 64 * 1024; + private static readonly TimeSpan InspectionTimeout = TimeSpan.FromMilliseconds(500); + + private readonly FlowRegistry _flows; + private readonly string _proxyHost; + private readonly int _proxyPort; + private readonly FilterEngine? _managedEngine; + private readonly CancellationTokenSource _shutdown = new(); + private readonly Socket _listener; + private Task? _acceptLoop; + + public TransparentTcpRelay(FlowRegistry flows, StartRequest request) + { + ArgumentNullException.ThrowIfNull(flows); + ArgumentNullException.ThrowIfNull(request); + + _flows = flows; + _proxyHost = request.LocalProxyHost; + _proxyPort = request.LocalProxyPort; + ManagedRoutingDto managed = request.ManagedRouting ?? new ManagedRoutingDto(); + if (managed.Enabled) + { + _managedEngine = FilterEngine.Compile( + managed.DefaultRules ?? [], + managed.UserRules ?? [], + out FilterCompilationReport report); + ManagedRoutingRuleCount = report.DefaultRuleCount + report.UserRuleCount; + } + + _listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) + { + DualMode = true, + NoDelay = true, + }; + _listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + _listener.Listen(1024); + Port = ((IPEndPoint)_listener.LocalEndPoint!).Port; + } + + public int Port { get; } + public bool ManagedRoutingActive => _managedEngine is not null; + public int ManagedRoutingRuleCount { get; private set; } + + public void Start() => _acceptLoop = AcceptLoopAsync(); + + private async Task AcceptLoopAsync() + { + while (!_shutdown.IsCancellationRequested) + { + Socket client; + try + { + client = await _listener.AcceptAsync(_shutdown.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + + _ = HandleConnectionAsync(client); + } + } + + private async Task HandleConnectionAsync(Socket client) + { + using (client) + { + client.NoDelay = true; + if (client.RemoteEndPoint is not IPEndPoint peer || client.LocalEndPoint is not IPEndPoint local) + { + return; + } + + IPAddress remoteAddress = Normalize(peer.Address); + IPAddress localAddress = Normalize(local.Address); + if (!_flows.TryGetReflected(6, localAddress, checked((ushort)peer.Port), remoteAddress, out FlowState? state) + || state is null) + { + return; + } + + RouteAction route = state.Route; + byte[] bufferedPrefix = []; + if (route == RouteAction.Deferred) + { + (route, bufferedPrefix) = await ResolveDeferredRouteAsync(client, state).ConfigureAwait(false); + } + + if (route == RouteAction.Block) + { + return; + } + + using Socket upstream = route == RouteAction.Proxy + ? await Socks5Connector.ConnectAsync( + _proxyHost, + _proxyPort, + state.Key.RemoteAddress, + state.Key.RemotePort, + _shutdown.Token).ConfigureAwait(false) + : await ConnectDirectAsync( + state.Key.RemoteAddress, + state.Key.RemotePort, + _shutdown.Token).ConfigureAwait(false); + + using NetworkStream clientStream = new(client, ownsSocket: false); + using NetworkStream upstreamStream = new(upstream, ownsSocket: false); + if (bufferedPrefix.Length > 0) + { + await upstreamStream.WriteAsync(bufferedPrefix, _shutdown.Token).ConfigureAwait(false); + } + + Task a = PumpAsync(clientStream, upstreamStream, _shutdown.Token); + Task b = PumpAsync(upstreamStream, clientStream, _shutdown.Token); + await Task.WhenAny(a, b).ConfigureAwait(false); + } + } + + private async Task<(RouteAction Route, byte[] Prefix)> ResolveDeferredRouteAsync(Socket client, FlowState state) + { + if (_managedEngine is null) + { + return (NormalizeFallback(state.FallbackRoute), []); + } + + using MemoryStream prefix = new(); + byte[] buffer = new byte[4096]; + using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(_shutdown.Token); + timeout.CancelAfter(InspectionTimeout); + + while (prefix.Length < MaxInspectionBytes) + { + byte[] current = prefix.ToArray(); + TcpDestinationInspectionStatus status = InitialTcpProtocolInspector.Inspect( + current, + state.Key.RemotePort, + out TcpDestinationIdentity identity); + if (status == TcpDestinationInspectionStatus.Identified) + { + FilterRoutingDecision decision = _managedEngine.Evaluate(identity.Url, identity.Host); + return ( + decision.Action == FilterRoutingAction.Proxy ? RouteAction.Proxy : RouteAction.Direct, + prefix.ToArray()); + } + + if (status == TcpDestinationInspectionStatus.Unknown) + { + return (ResolveManagedFallback(state), prefix.ToArray()); + } + + int remaining = checked(MaxInspectionBytes - (int)prefix.Length); + int read; + try + { + read = await client.ReceiveAsync( + buffer.AsMemory(0, Math.Min(buffer.Length, remaining)), + SocketFlags.None, + timeout.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!_shutdown.IsCancellationRequested) + { + return (ResolveManagedFallback(state), prefix.ToArray()); + } + + if (read == 0) + { + return (ResolveManagedFallback(state), prefix.ToArray()); + } + prefix.Write(buffer, 0, read); + } + + return (ResolveManagedFallback(state), prefix.ToArray()); + } + + private RouteAction ResolveManagedFallback(FlowState state) + { + if (_managedEngine is null) + { + return NormalizeFallback(state.FallbackRoute); + } + + string host = state.Key.RemoteAddress.ToString(); + string authority = state.Key.RemoteAddress.AddressFamily == AddressFamily.InterNetworkV6 + ? "[" + host + "]" + : host; + string scheme = state.Key.RemotePort == 80 ? "http" : "https"; + int defaultPort = scheme == "http" ? 80 : 443; + if (state.Key.RemotePort != defaultPort) + { + authority += ":" + state.Key.RemotePort.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + FilterRoutingDecision decision = _managedEngine.Evaluate(scheme + "://" + authority + "/", host); + return decision.Action == FilterRoutingAction.Proxy ? RouteAction.Proxy : RouteAction.Direct; + } + + private static RouteAction NormalizeFallback(RouteAction route) + => route switch + { + RouteAction.Proxy => RouteAction.Proxy, + RouteAction.Block => RouteAction.Block, + _ => RouteAction.Direct, + }; + + private static async Task ConnectDirectAsync( + IPAddress remoteAddress, + ushort remotePort, + CancellationToken cancellationToken) + { + Socket socket = new(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + try + { + await socket.ConnectAsync(new IPEndPoint(remoteAddress, remotePort), cancellationToken).ConfigureAwait(false); + return socket; + } + catch + { + socket.Dispose(); + throw; + } + } + + private static async Task PumpAsync(Stream source, Stream destination, CancellationToken token) + { + byte[] buffer = new byte[64 * 1024]; + try + { + while (true) + { + int read = await source.ReadAsync(buffer, token).ConfigureAwait(false); + if (read == 0) + { + return; + } + + await destination.WriteAsync(buffer.AsMemory(0, read), token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (IOException) + { + } + catch (SocketException) + { + } + } + + private static IPAddress Normalize(IPAddress address) + => address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address; + + public async ValueTask DisposeAsync() + { + _shutdown.Cancel(); + _listener.Dispose(); + if (_acceptLoop is not null) + { + try + { + await _acceptLoop.ConfigureAwait(false); + } + catch + { + } + } + _shutdown.Dispose(); + } +} diff --git a/Shadowsocks.NetworkService/Routing/TransparentUdpRelay.cs b/Shadowsocks.NetworkService/Routing/TransparentUdpRelay.cs new file mode 100644 index 000000000..e6ac1dbd5 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/TransparentUdpRelay.cs @@ -0,0 +1,190 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; + +namespace Shadowsocks.NetworkService.Routing; + +internal sealed class TransparentUdpRelay : IAsyncDisposable +{ + private readonly FlowRegistry _flows; + private readonly IPEndPoint _localSocksEndpoint; + private readonly CancellationTokenSource _shutdown = new(); + private readonly Socket _listener; + private readonly ConcurrentDictionary _udpFlows = new(); + private Task? _receiveLoop; + + public TransparentUdpRelay(FlowRegistry flows, string proxyHost, int proxyPort) + { + _flows = flows; + IPAddress proxyAddress = IPAddress.TryParse(proxyHost, out IPAddress? parsed) + ? parsed + : Dns.GetHostAddresses(proxyHost).First(static ip => ip.AddressFamily == AddressFamily.InterNetwork); + _localSocksEndpoint = new IPEndPoint(proxyAddress, proxyPort); + + _listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) + { + DualMode = true, + }; + _listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + Port = ((IPEndPoint)_listener.LocalEndPoint!).Port; + } + + public int Port { get; } + + public void Start() => _receiveLoop = ReceiveLoopAsync(); + + private async Task ReceiveLoopAsync() + { + byte[] buffer = new byte[65535]; + EndPoint peerTemplate = new IPEndPoint(IPAddress.IPv6Any, 0); + while (!_shutdown.IsCancellationRequested) + { + try + { + SocketReceiveFromResult result = await _listener.ReceiveFromAsync(buffer, SocketFlags.None, peerTemplate, _shutdown.Token).ConfigureAwait(false); + if (result.RemoteEndPoint is not IPEndPoint peer) + { + continue; + } + + IPAddress remoteAddress = Normalize(peer.Address); + if (!_flows.TryGetByPeer(17, remoteAddress, checked((ushort)peer.Port), out FlowState? state) + || state is null) + { + continue; + } + + UdpFlow flow = _udpFlows.GetOrAdd(state.Key, key => new UdpFlow( + key, + _listener, + _localSocksEndpoint, + _shutdown.Token)); + await flow.SendAsync(buffer.AsMemory(0, result.ReceivedBytes)).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_shutdown.IsCancellationRequested) + { + break; + } + } + } + + private static IPAddress Normalize(IPAddress address) + => address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address; + + public async ValueTask DisposeAsync() + { + _shutdown.Cancel(); + _listener.Dispose(); + foreach (UdpFlow flow in _udpFlows.Values) + { + flow.Dispose(); + } + _udpFlows.Clear(); + if (_receiveLoop is not null) + { + try + { + await _receiveLoop.ConfigureAwait(false); + } + catch + { + } + } + _shutdown.Dispose(); + } + + private sealed class UdpFlow : IDisposable + { + private readonly FlowKey _flow; + private readonly Socket _transparentListener; + private readonly IPEndPoint _localSocksEndpoint; + private readonly CancellationToken _token; + private readonly Socket _socksSocket; + private readonly SemaphoreSlim _sendLock = new(1, 1); + private readonly Task _receiveLoop; + + public UdpFlow(FlowKey flow, Socket transparentListener, IPEndPoint localSocksEndpoint, CancellationToken token) + { + _flow = flow; + _transparentListener = transparentListener; + _localSocksEndpoint = localSocksEndpoint; + _token = token; + _socksSocket = new Socket(localSocksEndpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + _socksSocket.Bind(new IPEndPoint( + localSocksEndpoint.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Loopback : IPAddress.IPv6Loopback, + 0)); + _receiveLoop = ReceiveFromSocksAsync(); + } + + public async Task SendAsync(ReadOnlyMemory payload) + { + byte[] packet = Socks5Connector.BuildUdpDatagram( + new IPEndPoint(_flow.RemoteAddress, _flow.RemotePort), + payload.Span); + await _sendLock.WaitAsync(_token).ConfigureAwait(false); + try + { + await _socksSocket.SendToAsync(packet, SocketFlags.None, _localSocksEndpoint, _token).ConfigureAwait(false); + } + finally + { + _sendLock.Release(); + } + } + + private async Task ReceiveFromSocksAsync() + { + byte[] buffer = new byte[65535]; + EndPoint sender = new IPEndPoint(_localSocksEndpoint.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, 0); + while (!_token.IsCancellationRequested) + { + try + { + SocketReceiveFromResult result = await _socksSocket.ReceiveFromAsync(buffer, SocketFlags.None, sender, _token).ConfigureAwait(false); + if (!Socks5Connector.TryParseUdpDatagram( + buffer.AsSpan(0, result.ReceivedBytes), + out IPEndPoint? source, + out byte[] payload) + || source is null) + { + continue; + } + + // Deliberately send toward the spoofed remote endpoint. WinDivert + // catches this outbound datagram and reflects it back into the local + // stack with source address/port restored to the remote peer. + IPEndPoint fakePeer = new(source.Address, _flow.LocalPort); + await _transparentListener.SendToAsync(payload, SocketFlags.None, fakePeer, _token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException) when (_token.IsCancellationRequested) + { + break; + } + } + } + + public void Dispose() + { + _socksSocket.Dispose(); + _sendLock.Dispose(); + _ = _receiveLoop; + } + } +} diff --git a/Shadowsocks.NetworkService/Routing/WinDivertFlowObserver.cs b/Shadowsocks.NetworkService/Routing/WinDivertFlowObserver.cs new file mode 100644 index 000000000..e588e7966 --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/WinDivertFlowObserver.cs @@ -0,0 +1,85 @@ +#nullable enable +using System.ComponentModel; +using System.Runtime.InteropServices; +using Shadowsocks.NetworkService.WinDivert; + +namespace Shadowsocks.NetworkService.Routing; + +/// +/// Observes WinDivert FLOW events to learn endpoint ownership before NETWORK-layer +/// packets are classified. This removes most PID-attribution races; IP Helper table +/// lookup remains a fallback for flows that existed before this observer was opened. +/// +internal sealed class WinDivertFlowObserver : IAsyncDisposable +{ + private readonly CancellationTokenSource _shutdown = new(); + private nint _handle; + private Task? _loop; + + public Task Completion => _loop ?? Task.CompletedTask; + + public void Start() + { + _handle = WinDivertNative.Open( + "outbound and (tcp or udp)", + WinDivertLayer.Flow, + 101, + WinDivertFlags.Sniff | WinDivertFlags.ReceiveOnly); + if (WinDivertNative.IsInvalidHandle(_handle)) + throw new Win32Exception(Marshal.GetLastPInvokeError(), "WinDivert FLOW observer open failed."); + + _loop = Task.Run(ObserveLoop); + } + + private unsafe void ObserveLoop() + { + while (!_shutdown.IsCancellationRequested) + { + if (!WinDivertNative.Receive(_handle, null, 0, out _, out WinDivertAddress address)) + { + int error = Marshal.GetLastPInvokeError(); + if (_shutdown.IsCancellationRequested && error is 6 or 232 or 995) + return; + throw new Win32Exception(error, "WinDivert FLOW observer receive failed."); + } + + WinDivertDataFlow flow = address.Flow; + int processId = unchecked((int)flow.ProcessId); + if (processId <= 0 || flow.LocalPort == 0 || flow.Protocol is not 6 and not 17) + continue; + + switch (address.Event) + { + case WinDivertEvent.FlowEstablished: + ProcessAttributionCache.Observe(flow.Protocol, flow.LocalPort, processId, DateTime.UtcNow); + break; + case WinDivertEvent.FlowDeleted: + ProcessAttributionCache.Remove(flow.Protocol, flow.LocalPort, processId); + break; + } + } + } + + public async ValueTask DisposeAsync() + { + _shutdown.Cancel(); + if (!WinDivertNative.IsInvalidHandle(_handle)) + { + WinDivertNative.Shutdown(_handle, WinDivertShutdown.Receive); + WinDivertNative.Close(_handle); + _handle = 0; + } + + if (_loop is not null) + { + try + { + await _loop.ConfigureAwait(false); + } + catch + { + } + } + _shutdown.Dispose(); + } +} diff --git a/Shadowsocks.NetworkService/Routing/WinDivertTransparentRouter.cs b/Shadowsocks.NetworkService/Routing/WinDivertTransparentRouter.cs new file mode 100644 index 000000000..7171ec3ba --- /dev/null +++ b/Shadowsocks.NetworkService/Routing/WinDivertTransparentRouter.cs @@ -0,0 +1,422 @@ +using System.ComponentModel; +using System.Net; +using System.Runtime.InteropServices; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.WinDivert; + +namespace Shadowsocks.NetworkService.Routing; + +internal sealed class WinDivertTransparentRouter : IAsyncDisposable +{ + private const uint MaxPacket = 40 + 65535; + private readonly FlowRegistry _flows; + private readonly WinDivertFlowObserver _flowObserver = new(); + private readonly TransparentTcpRelay _tcpRelay; + private readonly TransparentUdpRelay _udpRelay; + private readonly TransparentDnsRelay? _dnsRelay; + private readonly TransparentDohRelay? _dohRelay; + private readonly DnsPolicyDto _dnsPolicy; + private readonly IpFragmentTracker _fragments = new(); + private readonly CancellationTokenSource _shutdown = new(); + private nint _handle; + private Task? _packetLoop; + private Task? _completion; + + public WinDivertTransparentRouter(StartRequest request) + { + ArgumentNullException.ThrowIfNull(request); + WinDivertNative.ConfigureDirectory(request.WinDivertDirectory); + ApplicationPolicy policy = new(request); + _flows = new FlowRegistry(policy); + _tcpRelay = new TransparentTcpRelay(_flows, request); + _udpRelay = new TransparentUdpRelay(_flows, request.LocalProxyHost, request.LocalProxyPort); + _dnsPolicy = request.DnsPolicy ?? new DnsPolicyDto(); + + if (_dnsPolicy.DnsCryptReady) + { + if (request.ExcludedProcessIds is null || !request.ExcludedProcessIds.Contains(_dnsPolicy.DnsCryptProcessId)) + { + throw new InvalidOperationException( + "DNSCrypt process must be excluded from WinDivert capture before DNS interception is enabled."); + } + _dnsRelay = new TransparentDnsRelay(_flows, _dnsPolicy.DnsCryptPort); + } + else if (_dnsPolicy.DirectDnsReady + && IPAddress.TryParse(_dnsPolicy.DirectDnsServer.Trim(), out IPAddress? directDnsAddress)) + { + var directDnsEndpoints = new List + { + new(directDnsAddress, 53), + }; + if (IPAddress.TryParse(_dnsPolicy.DirectDnsFallbackServer?.Trim(), out IPAddress? fallbackDnsAddress) + && !fallbackDnsAddress.Equals(directDnsAddress)) + { + directDnsEndpoints.Add(new IPEndPoint(fallbackDnsAddress, 53)); + } + _dnsRelay = _dnsPolicy.DirectDnsRouteThroughShadowsocks + ? new TransparentDnsRelay( + _flows, + directDnsEndpoints, + proxyHost: request.LocalProxyHost, + proxyPort: request.LocalProxyPort) + : new TransparentDnsRelay(_flows, directDnsEndpoints); + } + else if (_dnsPolicy.CustomDohReady) + { + _dohRelay = new TransparentDohRelay( + _flows, + _dnsPolicy.CustomDohUrl, + _dnsPolicy.CustomDohRouteThroughShadowsocks, + request.LocalProxyHost, + request.LocalProxyPort); + } + } + + public int TcpRedirectPort => _tcpRelay.Port; + public int UdpRedirectPort => _udpRelay.Port; + public bool ManagedRoutingActive => _tcpRelay.ManagedRoutingActive; + public int ManagedRoutingRuleCount => _tcpRelay.ManagedRoutingRuleCount; + public bool DnsInterceptionActive => _dnsPolicy.Mode switch + { + DnsPolicyMode.Direct => _dnsRelay is not null || _dnsPolicy.DirectDnsRouteThroughShadowsocks, + DnsPolicyMode.Proxy => true, + DnsPolicyMode.CustomDoh => _dohRelay is not null, + DnsPolicyMode.DnsCrypt => _dnsRelay is not null, + _ => false, + }; + public bool DnsFailClosedActive => _dnsPolicy.Mode switch + { + DnsPolicyMode.CustomDoh => _dohRelay is null && _dnsPolicy.FailClosed, + DnsPolicyMode.DnsCrypt => _dnsRelay is null && _dnsPolicy.FailClosed, + _ => false, + }; + public Task Completion => _completion ?? _packetLoop ?? Task.CompletedTask; + + public void Start() + { + // Open the FLOW observer before the NETWORK handle so new connections can be + // attributed to a PID before their packets reach the routing loop. + _flowObserver.Start(); + _tcpRelay.Start(); + _udpRelay.Start(); + _dnsRelay?.Start(); + _dohRelay?.Start(); + + // Normal loopback traffic is excluded so the local Shadowsocks and DNSCrypt + // endpoints cannot recurse. In DNSCrypt mode we still capture explicit loopback + // port-53 requests (for example a system DNS stub on 127.0.0.1). When redirecting, + // the DNS relay source ports are admitted as well so their spoofed replies can be + // reflected back with source port 53. + bool interceptLoopbackDns = _dnsRelay is not null + || _dnsPolicy.Mode is DnsPolicyMode.Proxy or DnsPolicyMode.CustomDoh or DnsPolicyMode.DnsCrypt + || (_dnsPolicy.Mode == DnsPolicyMode.Direct && _dnsPolicy.DirectDnsRouteThroughShadowsocks); + int dnsTcpRelayPort = _dnsRelay?.TcpPort ?? _dohRelay?.TcpPort ?? 0; + int dnsUdpRelayPort = _dnsRelay?.UdpPort ?? _dohRelay?.UdpPort ?? 0; + string filter = interceptLoopbackDns + ? dnsTcpRelayPort == 0 || dnsUdpRelayPort == 0 + ? "outbound and (tcp or udp or fragment) and (!loopback or fragment or tcp.DstPort == 53 or udp.DstPort == 53)" + : $"outbound and (tcp or udp or fragment) and (!loopback or fragment or tcp.DstPort == 53 or udp.DstPort == 53 " + + $"or tcp.SrcPort == {dnsTcpRelayPort} or udp.SrcPort == {dnsUdpRelayPort})" + : "outbound and !loopback and (tcp or udp or fragment)"; + _handle = WinDivertNative.Open( + filter, + WinDivertLayer.Network, + 100, + WinDivertFlags.None); + if (WinDivertNative.IsInvalidHandle(_handle)) + { + throw new Win32Exception(Marshal.GetLastPInvokeError(), "WinDivertOpen failed."); + } + + SetParamOrThrow(WinDivertParam.QueueLength, 8192); + SetParamOrThrow(WinDivertParam.QueueSize, 16 * 1024 * 1024); + SetParamOrThrow(WinDivertParam.QueueTime, 1000); + _packetLoop = Task.Run(PacketLoop); + _completion = Task.WhenAll(_packetLoop, _flowObserver.Completion); + } + + private unsafe void PacketLoop() + { + byte[] packet = new byte[checked((int)MaxPacket)]; + while (!_shutdown.IsCancellationRequested) + { + fixed (byte* packetPtr = packet) + { + if (!WinDivertNative.Receive(_handle, packetPtr, MaxPacket, out uint length, out WinDivertAddress address)) + { + int error = Marshal.GetLastPInvokeError(); + if (IsExpectedReceiveTermination(_shutdown.IsCancellationRequested, error)) + return; + + throw new Win32Exception(error, "WinDivertRecv failed while capture was active."); + } + + DateTime nowUtc = DateTime.UtcNow; + bool isFragment = IpFragmentTracker.TryParse( + new ReadOnlySpan(packetPtr, checked((int)length)), + out IpFragmentInfo fragment); + + if (isFragment && !fragment.IsFirstFragment) + { + // The local Windows stack normally emits the first fragment first. If an + // orphan/out-of-order continuation arrives, fail secure instead of letting + // an unclassified fragment bypass DNS/application routing. + if (_fragments.TryGetDisposition(fragment, nowUtc, out FragmentDisposition disposition) + && disposition == FragmentDisposition.Direct) + { + Reinject(packetPtr, length, ref address); + } + continue; + } + + if (!PacketView.TryParse(packetPtr, length, out PacketView view)) + { + if (isFragment) + { + // A first fragment without a complete TCP/UDP header cannot be safely + // classified or rewritten. Remember Drop for all remaining fragments. + _fragments.Record(fragment, FragmentDisposition.Drop, nowUtc); + } + else + { + Reinject(packetPtr, length, ref address); + } + continue; + } + + ushort sourcePort = view.SourcePort; + ushort destinationPort = view.DestinationPort; + ushort proxyRedirectPort = view.IsTcp + ? checked((ushort)_tcpRelay.Port) + : checked((ushort)_udpRelay.Port); + ushort dnsRedirectPort = view.IsTcp + ? checked((ushort)(_dnsRelay?.TcpPort ?? _dohRelay?.TcpPort ?? 0)) + : checked((ushort)(_dnsRelay?.UdpPort ?? _dohRelay?.UdpPort ?? 0)); + + if (isFragment) + { + FragmentDisposition disposition = EvaluateFirstFragment( + view, + proxyRedirectPort, + dnsRedirectPort); + _fragments.Record(fragment, disposition, nowUtc); + if (disposition == FragmentDisposition.Direct) + { + Reinject(packetPtr, length, ref address); + } + continue; + } + + // Response generated by one of the local transparent relays. Both the + // Shadowsocks relay and the DNS bridge use the same reflection mapping. + if (sourcePort == proxyRedirectPort || (dnsRedirectPort != 0 && sourcePort == dnsRedirectPort)) + { + if (_flows.TryGetReflected( + view.Protocol, + view.SourceAddress, + destinationPort, + view.DestinationAddress, + out FlowState? reflected) + && reflected is not null) + { + view.ReflectFromLocal(reflected.Key.RemotePort); + address.Outbound = false; + RecalculateChecksumsOrThrow(packetPtr, length, ref address); + Reinject(packetPtr, length, ref address); + } + // Unknown packets from a reserved transparent relay port are not + // allowed to escape to the network. + continue; + } + + FlowKey key = new( + view.Protocol, + view.SourceAddress, + sourcePort, + view.DestinationAddress, + destinationPort); + + if (destinationPort == 53) + { + FlowState dnsState = _flows.GetOrCreate(key); + DnsRouteDecision dnsDecision = DnsRoutingPolicy.Evaluate( + _dnsPolicy, + _flows.IsDnsInterceptionExempt(dnsState)); + switch (dnsDecision) + { + case DnsRouteDecision.Direct: + Reinject(packetPtr, length, ref address); + break; + case DnsRouteDecision.Proxy: + _flows.MarkReflected(dnsState); + view.ReflectToLocal(proxyRedirectPort); + address.Outbound = false; + RecalculateChecksumsOrThrow(packetPtr, length, ref address); + Reinject(packetPtr, length, ref address); + break; + case DnsRouteDecision.Block: + // Fail-closed: deliberately drop plaintext DNS while DNSCrypt + // is unavailable. No silent downgrade to the system resolver. + break; + case DnsRouteDecision.Redirect: + if (dnsRedirectPort == 0) + { + // Defensive fallback. The policy should only return Redirect + // when the DNS relay exists, but never leak if state diverges. + if (!_dnsPolicy.FailClosed) + { + Reinject(packetPtr, length, ref address); + } + break; + } + _flows.MarkReflected(dnsState); + view.ReflectToLocal(dnsRedirectPort); + address.Outbound = false; + RecalculateChecksumsOrThrow(packetPtr, length, ref address); + Reinject(packetPtr, length, ref address); + break; + } + continue; + } + + // DHCP, mDNS, LLMNR and local/private network traffic remain direct. + if (NetworkClassifier.IsInfrastructureTraffic(key)) + { + Reinject(packetPtr, length, ref address); + continue; + } + + FlowState state = _flows.GetOrCreate(key); + switch (state.Route) + { + case RouteAction.Direct: + case RouteAction.Default: + Reinject(packetPtr, length, ref address); + break; + case RouteAction.Block: + break; + case RouteAction.Proxy: + case RouteAction.Deferred: + view.ReflectToLocal(proxyRedirectPort); + address.Outbound = false; + RecalculateChecksumsOrThrow(packetPtr, length, ref address); + Reinject(packetPtr, length, ref address); + break; + } + } + } + } + + private FragmentDisposition EvaluateFirstFragment( + PacketView view, + ushort proxyRedirectPort, + ushort dnsRedirectPort) + { + ushort sourcePort = view.SourcePort; + ushort destinationPort = view.DestinationPort; + + // Local relay responses require address/port rewriting too. Do not partially + // rewrite a fragmented relay datagram. + if (sourcePort == proxyRedirectPort || (dnsRedirectPort != 0 && sourcePort == dnsRedirectPort)) + return FragmentDisposition.Drop; + + FlowKey key = new( + view.Protocol, + view.SourceAddress, + sourcePort, + view.DestinationAddress, + destinationPort); + + if (destinationPort == 53) + { + FlowState dnsState = _flows.GetOrCreate(key); + DnsRouteDecision dnsDecision = DnsRoutingPolicy.Evaluate( + _dnsPolicy, + _flows.IsDnsInterceptionExempt(dnsState)); + return IpFragmentTracker.ForDnsDecision(dnsDecision); + } + + if (NetworkClassifier.IsInfrastructureTraffic(key)) + return FragmentDisposition.Direct; + + FlowState state = _flows.GetOrCreate(key); + return IpFragmentTracker.ForRoute(state.Route); + } + + private void SetParamOrThrow(WinDivertParam parameter, ulong value) + { + if (!WinDivertNative.SetParam(_handle, parameter, value)) + { + throw new Win32Exception( + Marshal.GetLastPInvokeError(), + $"WinDivertSetParam failed for {parameter}."); + } + } + + private unsafe static void RecalculateChecksumsOrThrow( + byte* packet, + uint length, + ref WinDivertAddress address) + { + if (!WinDivertNative.CalculateChecksums(packet, length, ref address, 0)) + { + throw new Win32Exception( + Marshal.GetLastPInvokeError(), + "WinDivert failed to recalculate packet checksums."); + } + } + + private unsafe void Reinject(byte* packet, uint length, ref WinDivertAddress address) + { + bool succeeded = WinDivertNative.Send(_handle, packet, length, out uint sentLength, in address); + int error = succeeded ? 0 : Marshal.GetLastPInvokeError(); + ValidateSendResult(succeeded, sentLength, length, error); + } + + internal static bool IsExpectedReceiveTermination(bool shutdownRequested, int errorCode) + => shutdownRequested && errorCode is 6 or 232 or 995; + + internal static void ValidateSendResult(bool succeeded, uint sentLength, uint expectedLength, int errorCode) + { + if (!succeeded) + throw new Win32Exception(errorCode, "WinDivertSend failed while capture was active."); + + if (sentLength != expectedLength) + throw new IOException($"WinDivertSend injected {sentLength} of {expectedLength} bytes."); + } + + public async ValueTask DisposeAsync() + { + _shutdown.Cancel(); + if (!WinDivertNative.IsInvalidHandle(_handle)) + { + WinDivertNative.Shutdown(_handle, WinDivertShutdown.Both); + WinDivertNative.Close(_handle); + _handle = 0; + } + + if (_packetLoop is not null) + { + try + { + await _packetLoop.ConfigureAwait(false); + } + catch + { + } + } + + await _flowObserver.DisposeAsync().ConfigureAwait(false); + if (_dnsRelay is not null) + { + await _dnsRelay.DisposeAsync().ConfigureAwait(false); + } + if (_dohRelay is not null) + { + await _dohRelay.DisposeAsync().ConfigureAwait(false); + } + await _tcpRelay.DisposeAsync().ConfigureAwait(false); + await _udpRelay.DisposeAsync().ConfigureAwait(false); + _shutdown.Dispose(); + } +} diff --git a/Shadowsocks.NetworkService/Shadowsocks.NetworkService.csproj b/Shadowsocks.NetworkService/Shadowsocks.NetworkService.csproj new file mode 100644 index 000000000..3a70617aa --- /dev/null +++ b/Shadowsocks.NetworkService/Shadowsocks.NetworkService.csproj @@ -0,0 +1,58 @@ + + + WinExe + net10.0-windows10.0.19041.0 + Shadowsocks community + https://github.com/SlimRG/shadowsocks-reborn + git + Shadowsocks.NetworkService + Shadowsocks.NetworkService + x64 + x64 + win-x64 + true + enable + enable + $(DefineConstants);SHADOWSOCKS_NETWORKSERVICE_FILTER_ENGINE + true + app.manifest + false + + + + + true + true + true + true + false + true + None + false + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\bin\ProductPublish\$(Configuration)\win-x64\')) + + + + false + false + + + + + + + + + + + + + + diff --git a/Shadowsocks.NetworkService/WinDivert/PacketView.cs b/Shadowsocks.NetworkService/WinDivert/PacketView.cs new file mode 100644 index 000000000..ecaa9b498 --- /dev/null +++ b/Shadowsocks.NetworkService/WinDivert/PacketView.cs @@ -0,0 +1,160 @@ +using System.Buffers.Binary; +using System.Net; + +namespace Shadowsocks.NetworkService.WinDivert; + +internal unsafe readonly struct PacketView +{ + private readonly byte* _packet; + private readonly uint _packetLength; + private readonly byte* _ip; + private readonly byte* _ipv6; + private readonly byte* _transport; + private readonly byte* _payload; + + private PacketView( + byte* packet, + uint packetLength, + byte protocol, + byte* ip, + byte* ipv6, + byte* transport, + byte* payload, + uint payloadLength) + { + _packet = packet; + _packetLength = packetLength; + Protocol = protocol; + _ip = ip; + _ipv6 = ipv6; + _transport = transport; + _payload = payload; + PayloadLength = payloadLength; + } + + public byte Protocol { get; } + public uint PayloadLength { get; } + public bool IsTcp => Protocol == 6 && _transport != null; + public bool IsUdp => Protocol == 17 && _transport != null; + public bool IsIPv6 => _ipv6 != null; + + public IPAddress SourceAddress => ReadAddress(source: true); + public IPAddress DestinationAddress => ReadAddress(source: false); + + public ushort SourcePort => ReadPort(0); + public ushort DestinationPort => ReadPort(2); + + public ReadOnlySpan Payload => _payload is null || PayloadLength == 0 + ? ReadOnlySpan.Empty + : new ReadOnlySpan(_payload, checked((int)PayloadLength)); + + public static bool TryParse(byte* packet, uint packetLength, out PacketView view) + { + view = default; + if (!WinDivertNative.ParsePacket( + packet, + packetLength, + out nint ipHeader, + out nint ipv6Header, + out byte protocol, + 0, + 0, + out nint tcpHeader, + out nint udpHeader, + out nint data, + out uint dataLength, + 0, + 0)) + { + return false; + } + + nint transport = tcpHeader != 0 ? tcpHeader : udpHeader; + if (transport == 0 || (protocol != 6 && protocol != 17)) + { + return false; + } + + view = new PacketView( + packet, + packetLength, + protocol, + (byte*)ipHeader, + (byte*)ipv6Header, + (byte*)transport, + (byte*)data, + dataLength); + return true; + } + + public void ReflectToLocal(ushort localProxyPort) + { + SwapAddresses(); + WritePort(2, localProxyPort); + } + + public void ReflectFromLocal(ushort originalRemotePort) + { + SwapAddresses(); + WritePort(0, originalRemotePort); + } + + private IPAddress ReadAddress(bool source) + { + if (_ip != null) + { + int offset = source ? 12 : 16; + return new IPAddress(new ReadOnlySpan(_ip + offset, 4)); + } + + if (_ipv6 != null) + { + int offset = source ? 8 : 24; + return new IPAddress(new ReadOnlySpan(_ipv6 + offset, 16)); + } + + throw new InvalidOperationException("Packet has no IP header."); + } + + private void SwapAddresses() + { + if (_ip != null) + { + SwapBytes(_ip + 12, _ip + 16, 4); + return; + } + + if (_ipv6 != null) + { + SwapBytes(_ipv6 + 8, _ipv6 + 24, 16); + return; + } + + throw new InvalidOperationException("Packet has no IP header."); + } + + private ushort ReadPort(int offset) + { + if (_transport == null) + { + return 0; + } + + return BinaryPrimitives.ReadUInt16BigEndian(new ReadOnlySpan(_transport + offset, 2)); + } + + private void WritePort(int offset, ushort value) + { + BinaryPrimitives.WriteUInt16BigEndian(new Span(_transport + offset, 2), value); + } + + private static void SwapBytes(byte* left, byte* right, int length) + { + for (int i = 0; i < length; i++) + { + byte value = left[i]; + left[i] = right[i]; + right[i] = value; + } + } +} diff --git a/Shadowsocks.NetworkService/WinDivert/WinDivertNative.cs b/Shadowsocks.NetworkService/WinDivert/WinDivertNative.cs new file mode 100644 index 000000000..5a34d74bd --- /dev/null +++ b/Shadowsocks.NetworkService/WinDivert/WinDivertNative.cs @@ -0,0 +1,198 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Shadowsocks.NetworkService.WinDivert; + +internal enum WinDivertLayer : int +{ + Network = 0, + NetworkForward = 1, + Flow = 2, + Socket = 3, + Reflect = 4, +} + +internal enum WinDivertEvent : byte +{ + NetworkPacket = 0, + FlowEstablished = 1, + FlowDeleted = 2, + SocketBind = 3, + SocketConnect = 4, + SocketListen = 5, + SocketAccept = 6, + SocketClose = 7, + ReflectOpen = 8, + ReflectClose = 9, +} + +[Flags] +internal enum WinDivertFlags : ulong +{ + None = 0, + Sniff = 0x0001, + Drop = 0x0002, + ReceiveOnly = 0x0004, + SendOnly = 0x0008, + NoInstall = 0x0010, + Fragments = 0x0020, +} + +internal enum WinDivertShutdown : int +{ + Receive = 0x1, + Send = 0x2, + Both = 0x3, +} + +internal enum WinDivertParam : int +{ + QueueLength = 0, + QueueTime = 1, + QueueSize = 2, + VersionMajor = 3, + VersionMinor = 4, +} + +[StructLayout(LayoutKind.Sequential)] +internal struct WinDivertDataNetwork +{ + public uint IfIdx; + public uint SubIfIdx; +} + +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct WinDivertDataFlow +{ + public ulong EndpointId; + public ulong ParentEndpointId; + public uint ProcessId; + public fixed uint LocalAddr[4]; + public fixed uint RemoteAddr[4]; + public ushort LocalPort; + public ushort RemotePort; + public byte Protocol; +} + +[StructLayout(LayoutKind.Explicit, Size = 80)] +internal unsafe struct WinDivertAddress +{ + [FieldOffset(0)] public long Timestamp; + [FieldOffset(8)] public uint BitFields; + [FieldOffset(12)] public uint Reserved2; + [FieldOffset(16)] public WinDivertDataNetwork Network; + [FieldOffset(16)] public WinDivertDataFlow Flow; + [FieldOffset(16)] public fixed byte Data[64]; + + public readonly WinDivertEvent Event => (WinDivertEvent)((BitFields >> 8) & 0xFF); + + public bool Outbound + { + readonly get => (BitFields & (1u << 17)) != 0; + set + { + if (value) + { + BitFields |= 1u << 17; + } + else + { + BitFields &= ~(1u << 17); + } + } + } + + public readonly bool IPv6 => (BitFields & (1u << 20)) != 0; +} + +internal static unsafe partial class WinDivertNative +{ + private static string? _directory; + private static nint _loadedModule; + private static int _resolverInstalled; + + internal static void ConfigureDirectory(string directory) + { + ArgumentException.ThrowIfNullOrWhiteSpace(directory); + string fullPath = Path.GetFullPath(directory); + string dllPath = Path.Combine(fullPath, "WinDivert.dll"); + string driverPath = Path.Combine(fullPath, "WinDivert64.sys"); + if (!File.Exists(dllPath)) + { + throw new FileNotFoundException("WinDivert.dll was not found.", dllPath); + } + if (!File.Exists(driverPath)) + { + throw new FileNotFoundException("WinDivert64.sys was not found.", driverPath); + } + + _directory = fullPath; + if (Interlocked.Exchange(ref _resolverInstalled, 1) == 0) + { + NativeLibrary.SetDllImportResolver(typeof(WinDivertNative).Assembly, ResolveLibrary); + } + } + + private static nint ResolveLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (!string.Equals(libraryName, "WinDivert.dll", StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + + if (_loadedModule != 0) + { + return _loadedModule; + } + + string directory = _directory ?? throw new InvalidOperationException("WinDivert directory is not configured."); + _loadedModule = NativeLibrary.Load(Path.Combine(directory, "WinDivert.dll")); + return _loadedModule; + } + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertOpen", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial nint Open(string filter, WinDivertLayer layer, short priority, WinDivertFlags flags); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertRecv", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Receive(nint handle, void* packet, uint packetLength, out uint receivedLength, out WinDivertAddress address); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertSend", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Send(nint handle, void* packet, uint packetLength, out uint sentLength, in WinDivertAddress address); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertShutdown", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Shutdown(nint handle, WinDivertShutdown how); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertClose", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Close(nint handle); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertSetParam", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetParam(nint handle, WinDivertParam parameter, ulong value); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertHelperParsePacket", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool ParsePacket( + void* packet, + uint packetLength, + out nint ipHeader, + out nint ipv6Header, + out byte protocol, + nint icmpHeader, + nint icmpv6Header, + out nint tcpHeader, + out nint udpHeader, + out nint data, + out uint dataLength, + nint next, + nint nextLength); + + [LibraryImport("WinDivert.dll", EntryPoint = "WinDivertHelperCalcChecksums", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CalculateChecksums(void* packet, uint packetLength, ref WinDivertAddress address, ulong flags); + + internal static bool IsInvalidHandle(nint handle) => handle == -1 || handle == 0; +} diff --git a/Shadowsocks.NetworkService/app.manifest b/Shadowsocks.NetworkService/app.manifest new file mode 100644 index 000000000..3fe8ab272 --- /dev/null +++ b/Shadowsocks.NetworkService/app.manifest @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Shadowsocks.UnitTests/.editorconfig b/Shadowsocks.UnitTests/.editorconfig new file mode 100644 index 000000000..c9225fdf4 --- /dev/null +++ b/Shadowsocks.UnitTests/.editorconfig @@ -0,0 +1,6 @@ +# Unit tests intentionally use Method_Scenario_Result names for scanability. +# Small literal arrays are local fixtures; hoisting them to static fields reduces test readability +# without a meaningful runtime benefit. These rules remain enabled for production projects. +[*.cs] +dotnet_diagnostic.CA1707.severity = none +dotnet_diagnostic.CA1861.severity = none diff --git a/Shadowsocks.UnitTests/AppSettingsTest.cs b/Shadowsocks.UnitTests/AppSettingsTest.cs new file mode 100644 index 000000000..5a1b8457c --- /dev/null +++ b/Shadowsocks.UnitTests/AppSettingsTest.cs @@ -0,0 +1,20 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Core.Settings; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class AppSettingsTest +{ + [TestMethod] + public void EmbeddedLoggingSettingsAreAvailable() + { + LoggingSettings logging = AppSettings.Logging; + + Assert.IsFalse(string.IsNullOrWhiteSpace(logging.FilePath)); + Assert.IsFalse(string.IsNullOrWhiteSpace(logging.FallbackFilePath)); + Assert.IsFalse(string.IsNullOrWhiteSpace(logging.MinimumLevel)); + Assert.IsFalse(string.IsNullOrWhiteSpace(logging.VerboseMinimumLevel)); + Assert.IsFalse(string.IsNullOrWhiteSpace(logging.Layout)); + } +} diff --git a/Shadowsocks.UnitTests/CleanStorageSessionTests.cs b/Shadowsocks.UnitTests/CleanStorageSessionTests.cs new file mode 100644 index 000000000..97fcc966f --- /dev/null +++ b/Shadowsocks.UnitTests/CleanStorageSessionTests.cs @@ -0,0 +1,71 @@ +#nullable enable +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class CleanStorageSessionTests + { + [TestMethod] + public void SessionOwnsExclusiveLockAndDeletesTreeOnDispose() + { + string sessionsRoot = Path.Combine( + Path.GetTempPath(), + "Shadowsocks.UnitTests", + "CleanSessions", + Guid.NewGuid().ToString("N")); + DateTime nowUtc = new(2026, 8, 18, 12, 34, 56, DateTimeKind.Utc); + Guid sessionId = Guid.Parse("9f14432d-a114-47e6-a120-29511dc39887"); + + try + { + CleanStorageSession session = CleanStorageSession.Create( + sessionsRoot, + nowUtc, + 4242, + sessionId); + string sessionRoot = session.Root; + string expectedName = "20260818123456-4242-9f14432da11447e6a12029511dc39887"; + Assert.AreEqual(expectedName, Path.GetFileName(sessionRoot)); + Assert.IsTrue(File.Exists(Path.Combine(sessionRoot, CleanStorageSession.LockFileName))); + Assert.IsTrue(CleanStorageSession.IsActive(sessionRoot)); + + string nested = Path.Combine(sessionRoot, "Runtime", "DNSCryptProxy"); + Directory.CreateDirectory(nested); + File.WriteAllText(Path.Combine(nested, "dnscrypt-proxy.toml"), "test"); + + session.Dispose(); + session.Dispose(); // Cleanup is idempotent during duplicate exit notifications. + Assert.IsFalse(Directory.Exists(sessionRoot)); + } + finally + { + CleanStorageSession.TryDeleteDirectory(sessionsRoot); + } + } + + [TestMethod] + public void UnlockedSessionDirectoryIsNotReportedAsActive() + { + string directory = Path.Combine( + Path.GetTempPath(), + "Shadowsocks.UnitTests", + "CleanUnlocked", + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + File.WriteAllText(Path.Combine(directory, CleanStorageSession.LockFileName), string.Empty); + + try + { + Assert.IsFalse(CleanStorageSession.IsActive(directory)); + } + finally + { + CleanStorageSession.TryDeleteDirectory(directory); + } + } + } +} diff --git a/Shadowsocks.UnitTests/ConfigurationServerNamingTests.cs b/Shadowsocks.UnitTests/ConfigurationServerNamingTests.cs new file mode 100644 index 000000000..f48697bb0 --- /dev/null +++ b/Shadowsocks.UnitTests/ConfigurationServerNamingTests.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public sealed class ConfigurationServerNamingTests +{ + [TestMethod] + public void EnsureServerNames_AssignsUniqueNamesWithoutOverwritingExplicitNames() + { + var servers = new List + { + new() { server = "one.example", ServerPort = 8388, password = "test", remarks = "Primary" }, + new() { server = "two.example", ServerPort = 8388, password = "test" }, + new() { server = "three.example", ServerPort = 8388, password = "test", remarks = "Server 1" }, + new() { server = "four.example", ServerPort = 8388, password = "test" }, + }; + + Configuration.EnsureServerNames(servers); + + Assert.AreEqual("Primary", servers[0].remarks); + Assert.AreEqual("Server 2", servers[1].remarks); + Assert.AreEqual("Server 1", servers[2].remarks); + Assert.AreEqual("Server 3", servers[3].remarks); + } + + [TestMethod] + public void EnsureServerNames_DoesNotNameBlankPlaceholderServer() + { + var servers = new List { new() }; + + Configuration.EnsureServerNames(servers); + + Assert.AreEqual(string.Empty, servers[0].remarks); + } +} diff --git a/Shadowsocks.UnitTests/ConfigurationStateTest.cs b/Shadowsocks.UnitTests/ConfigurationStateTest.cs new file mode 100644 index 000000000..b0d0c131b --- /dev/null +++ b/Shadowsocks.UnitTests/ConfigurationStateTest.cs @@ -0,0 +1,157 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class ConfigurationStateTest + { + [TestMethod] + public void EmptyConfigurationHasNoSyntheticServer() + { + Configuration configuration = new(); + Configuration.Process(ref configuration); + + Assert.AreEqual(0, configuration.configs.Count); + Assert.AreEqual(-1, configuration.index); + Assert.IsFalse(configuration.Enabled); + Assert.IsFalse(configuration.GetCurrentServer().IsConfigured); + Assert.IsFalse(configuration.HasConfiguredServer); + } + + [TestMethod] + public void LegacyBlankPlaceholderIsRemovedWithoutChangingSelectedRealServer() + { + Configuration configuration = new() + { + index = 1, + configs = + [ + new Server(), + new Server + { + server = "127.0.0.1", + ServerPort = 8388, + password = "test", + method = Server.DefaultMethod, + }, + ], + }; + + Configuration.Process(ref configuration); + + Assert.AreEqual(1, configuration.configs.Count); + Assert.AreEqual(0, configuration.index); + Assert.AreEqual("127.0.0.1", configuration.GetCurrentServer().server); + Assert.IsTrue(configuration.HasConfiguredServer); + } + + [TestMethod] + public void IncompleteServerCredentialsAreNotConfigured() + { + Server server = new() + { + server = "127.0.0.1", + ServerPort = 8388, + password = string.Empty, + method = Server.DefaultMethod, + }; + + Assert.IsFalse(server.IsConfigured); + server.password = "test"; + Assert.IsTrue(server.IsConfigured); + } + + + [TestMethod] + public void IncompleteSelectedServerDisablesProxyButPreservesDnsPreference() + { + Configuration configuration = new() + { + Enabled = true, + dnsPolicy = new Shadowsocks.Controller.Traffic.DnsPolicyConfig + { + mode = Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + }, + }; + configuration.configs.Add(new Server + { + server = "127.0.0.1", + ServerPort = 8388, + password = string.Empty, + }); + + Configuration.Process(ref configuration); + + Assert.IsFalse(configuration.HasConfiguredServer); + Assert.IsFalse(configuration.Enabled); + Assert.AreEqual(Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, configuration.dnsPolicy.mode); + } + + [TestMethod] + [DataRow("System", "System")] + [DataRow("light", "Light")] + [DataRow(" DARK ", "Dark")] + [DataRow("unexpected", "System")] + public void UiThemeIsNormalized(string input, string expected) + { + Configuration configuration = new(); + configuration.uiTheme = input; + + Configuration.Process(ref configuration); + + Assert.AreEqual(expected, configuration.uiTheme); + } + + [TestMethod] + public void UpdateCheckAtStartupIsEnabledByDefault() + { + Configuration configuration = new(); + + Assert.IsTrue(configuration.autoCheckUpdate); + } + + [TestMethod] + public void DnsDiagnosticLoggingIsDisabledByDefault() + { + Configuration configuration = new(); + + Assert.IsFalse(configuration.showDnsLogs); + } + + + [TestMethod] + public void EnabledUsesLegacyJsonPropertyName() + { + Configuration configuration = new() { Enabled = true }; + + string json = JsonConvert.SerializeObject(configuration); + Configuration roundTrip = JsonConvert.DeserializeObject(json); + + StringAssert.Contains(json, "\"enabled\":true"); + Assert.IsFalse(json.Contains("\"Enabled\"", StringComparison.Ordinal)); + Assert.IsNotNull(roundTrip); + Assert.IsTrue(roundTrip.Enabled); + } + + [TestMethod] + public void RealServerIsConfigured() + { + Configuration configuration = new(); + configuration.configs.Add(new Server + { + server = "127.0.0.1", + ServerPort = 8388, + password = "test", + method = Server.DefaultMethod, + }); + + Configuration.Process(ref configuration); + + Assert.IsTrue(configuration.GetCurrentServer().IsConfigured); + Assert.IsTrue(configuration.HasConfiguredServer); + } + } +} diff --git a/Shadowsocks.UnitTests/CryptoTest.cs b/Shadowsocks.UnitTests/CryptoTest.cs new file mode 100644 index 000000000..37514ce11 --- /dev/null +++ b/Shadowsocks.UnitTests/CryptoTest.cs @@ -0,0 +1,89 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Encryption; +using Shadowsocks.Encryption.AEAD; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class CryptoTest + { + private static readonly byte[] Plaintext = Encoding.ASCII.GetBytes("Shadowsocks BCL AEAD test vector."); + + [TestMethod] + public void Aes128GcmMatchesKnownVector() + { + AssertCipherVector( + "aes-128-gcm", + 16, + "bba8aa0b54856cc1a4b63f7e8970fbf9b6e66699924451cda14fea4a140c83f76ea29a8f2cb2749f8773bf9a63e6b34e00"); + } + + [TestMethod] + public void Aes192GcmMatchesKnownVector() + { + AssertCipherVector( + "aes-192-gcm", + 24, + "2b528572ee8a4f9638e4f59dfbd9b57d00e7a6c6924d0f4ee22f54b61dc4533b3d57bd9eb8329953e5f314b3fa60a03410"); + } + + [TestMethod] + public void Aes256GcmMatchesKnownVector() + { + AssertCipherVector( + "aes-256-gcm", + 32, + "b3b8c1facbe8751d4d7ce600f6cd5fa9cd8273d3e48139b2eb4d2ae7876d819d12776ded0d6194192833f2a25b12113581"); + } + + [TestMethod] + public void ChaCha20Poly1305MatchesKnownVector() + { + if (!ChaCha20Poly1305.IsSupported) + { + Assert.Inconclusive("ChaCha20-Poly1305 is not supported by this Windows version."); + } + + AssertCipherVector( + "chacha20-ietf-poly1305", + 32, + "2977565a6f87f83abd2a2e01a0524eb0f30a977d31e52368cf4b1491f3ea4d8a583f7dc40bba501dd5bfb38aa5734f2f00"); + } + + [TestMethod] + public void FactoryDoesNotRegisterXChaCha() + { + string registered = EncryptorFactory.DumpRegisteredEncryptor(); + Assert.IsFalse(registered.Contains("xchacha20-ietf-poly1305", StringComparison.Ordinal)); + Assert.IsTrue(registered.Contains("aes-256-gcm=>AEADBclEncryptor", StringComparison.Ordinal)); + Assert.IsTrue(registered.Contains("chacha20-ietf-poly1305=>AEADBclEncryptor", StringComparison.Ordinal)); + } + + private static void AssertCipherVector(string method, int saltLength, string expectedHex) + { + using var encryptor = new AEADBclEncryptor(method, "test-password"); + byte[] salt = new byte[saltLength]; + for (int i = 0; i < salt.Length; i++) + salt[i] = (byte)i; + + encryptor.InitCipher(salt, true, true); + byte[] ciphertext = new byte[Plaintext.Length + 16]; + uint ciphertextLength = 0; + encryptor.cipherEncrypt(Plaintext, (uint)Plaintext.Length, ciphertext, ref ciphertextLength); + + Assert.AreEqual(ciphertext.Length, (int)ciphertextLength); + Assert.AreEqual(expectedHex, Convert.ToHexString(ciphertext).ToLowerInvariant()); + + encryptor.InitCipher(salt, false, true); + byte[] decrypted = new byte[Plaintext.Length]; + uint plaintextLength = 0; + encryptor.cipherDecrypt(ciphertext, ciphertextLength, decrypted, ref plaintextLength); + + Assert.AreEqual(Plaintext.Length, (int)plaintextLength); + CollectionAssert.AreEqual(Plaintext, decrypted); + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCaptureStage4Tests.cs b/Shadowsocks.UnitTests/DnsCaptureStage4Tests.cs new file mode 100644 index 000000000..c59f506e5 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCaptureStage4Tests.cs @@ -0,0 +1,626 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Model; +using Shadowsocks.NetworkService; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.Routing; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class DnsCaptureStage4Tests +{ + [TestMethod] + public void DnsCaptureRuntimeState_ReadyOnlyWithPortAndPid() + { + Assert.IsTrue(new DnsCaptureRuntimeState(5300, 42).IsReady); + Assert.IsFalse(new DnsCaptureRuntimeState(0, 42).IsReady); + Assert.IsFalse(new DnsCaptureRuntimeState(5300, 0).IsReady); + } + + [TestMethod] + public void DnsRoutingPolicy_SystemDns_IsDirect() + { + DnsPolicyDto policy = new() { Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.System }; + Assert.AreEqual(DnsRouteDecision.Direct, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: false)); + } + + [TestMethod] + public void DnsRoutingPolicy_DnsCryptExemptProcess_IsDirect() + { + DnsPolicyDto policy = ReadyDnsCryptPolicy(); + Assert.AreEqual(DnsRouteDecision.Direct, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: true)); + } + + [TestMethod] + public void DnsRoutingPolicy_ReadyDnsCrypt_Redirects() + { + DnsPolicyDto policy = ReadyDnsCryptPolicy(); + Assert.AreEqual(DnsRouteDecision.Redirect, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: false)); + } + + [TestMethod] + public void DnsRoutingPolicy_UnavailableFailClosed_Blocks() + { + DnsPolicyDto policy = new() + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.DnsCrypt, + FailClosed = true, + }; + Assert.AreEqual(DnsRouteDecision.Block, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: false)); + } + + [TestMethod] + public void DnsRoutingPolicy_DnsCryptNeverFallsBackToPlaintext() + { + DnsPolicyDto policy = new() + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.DnsCrypt, + FailClosed = false, // Legacy/malformed IPC must not weaken DNSCrypt. + }; + Assert.AreEqual(DnsRouteDecision.Block, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: false)); + } + + [TestMethod] + public void NetworkClassifier_DhcpMdnsAndLlmnrRemainInfrastructureTraffic() + { + foreach (ushort port in new ushort[] { 67, 68, 5353, 5355 }) + { + FlowKey flow = new(17, IPAddress.Parse("192.0.2.10"), 50000, IPAddress.Parse("224.0.0.251"), port); + Assert.IsTrue(NetworkClassifier.IsInfrastructureTraffic(flow), $"UDP/{port} must remain direct infrastructure traffic."); + } + } + + + [TestMethod] + public void DnsRoutingPolicy_DirectAndProxyModesAreActionable() + { + Assert.AreEqual( + DnsRouteDecision.Direct, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto { Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.Direct }, false)); + Assert.AreEqual( + DnsRouteDecision.Proxy, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto { Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.Proxy }, false)); + } + + [TestMethod] + public void DnsRoutingPolicy_DirectCustomServerRedirects() + { + Assert.AreEqual( + DnsRouteDecision.Redirect, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.Direct, + DirectDnsServer = "1.1.1.1", + }, false)); + Assert.AreEqual( + DnsRouteDecision.Direct, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.Direct, + DirectDnsServer = "not-an-ip", + }, false)); + } + + [TestMethod] + public void DnsRoutingPolicy_DirectDnsCanRouteOriginalDestinationThroughShadowsocks() + { + Assert.AreEqual( + DnsRouteDecision.Proxy, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.Direct, + DirectDnsRouteThroughShadowsocks = true, + }, false)); + } + + [TestMethod] + public void TransparentDnsRelay_UsesWildcardBindForRemoteDnsServer() + { + Assert.AreEqual(IPAddress.Any, TransparentDnsRelay.SelectUdpBindAddress(IPAddress.Parse("1.1.1.1"))); + Assert.AreEqual(IPAddress.IPv6Any, TransparentDnsRelay.SelectUdpBindAddress(IPAddress.Parse("2606:4700:4700::1111"))); + Assert.AreEqual(IPAddress.Loopback, TransparentDnsRelay.SelectUdpBindAddress(IPAddress.Loopback)); + Assert.AreEqual(IPAddress.IPv6Loopback, TransparentDnsRelay.SelectUdpBindAddress(IPAddress.IPv6Loopback)); + } + + [TestMethod] + public void DnsRoutingPolicy_CustomDohRequiresValidHttpsEndpoint() + { + Assert.AreEqual( + DnsRouteDecision.Redirect, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.CustomDoh, + CustomDohUrl = "https://dns.example/dns-query", + FailClosed = true, + }, false)); + Assert.AreEqual( + DnsRouteDecision.Block, + DnsRoutingPolicy.Evaluate(new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.CustomDoh, + CustomDohUrl = "http://dns.example/dns-query", + FailClosed = true, + }, false)); + } + + [TestMethod] + public void NetworkClassifier_Dns53_IsHandledByDnsPolicyInsteadOfInfrastructureBypass() + { + FlowKey flow = new(17, IPAddress.Parse("192.0.2.10"), 50000, IPAddress.Parse("9.9.9.9"), 53); + Assert.IsFalse(NetworkClassifier.IsInfrastructureTraffic(flow)); + } + + [TestMethod] + public void AdminCaptureRequest_ContainsRuntimeDnsPolicyAndExclusion() + { + Configuration configuration = new() + { + dnsPolicy = new DnsPolicyConfig + { + mode = Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + directDnsServer = "9.9.9.9", + directDnsFallbackServer = "149.112.112.112", + directDnsRouteThroughShadowsocks = true, + customDohUrl = "https://dns.example/dns-query", + customDohRouteThroughShadowsocks = true, + dnsCrypt = new DnsCryptConfig { failClosed = true }, + }, + }; + + string json = AdminCaptureManager.BuildStartRequest( + configuration, + @"C:\\WinDivert", + new[] { 11, 22 }, + new DnsCaptureRuntimeState(38471, 12345)); + JObject request = JObject.Parse(json); + + Assert.AreEqual(4, request["dnsPolicy"]!["mode"]!.Value()); + Assert.AreEqual("9.9.9.9", request["dnsPolicy"]!["directDnsServer"]!.Value()); + Assert.AreEqual("149.112.112.112", request["dnsPolicy"]!["directDnsFallbackServer"]!.Value()); + Assert.IsTrue(request["dnsPolicy"]!["directDnsRouteThroughShadowsocks"]!.Value()); + Assert.AreEqual("https://dns.example/dns-query", request["dnsPolicy"]!["customDohUrl"]!.Value()); + Assert.IsTrue(request["dnsPolicy"]!["customDohRouteThroughShadowsocks"]!.Value()); + Assert.AreEqual(38471, request["dnsPolicy"]!["dnsCryptPort"]!.Value()); + Assert.AreEqual(12345, request["dnsPolicy"]!["dnsCryptProcessId"]!.Value()); + Assert.IsTrue(request["dnsPolicy"]!["failClosed"]!.Value()); + StringAssert.Contains(request["dnsPolicy"]!["dnsCryptComponentRoot"]!.Value()!, "DNSCryptProxy"); + CollectionAssert.Contains(request["excludedProcessIds"]!.Values().ToList(), 12345); + + StartRequest parsed = JsonSerializer.Deserialize(json, NetworkServiceJsonContext.Default.StartRequest); + Assert.IsNotNull(parsed); + Assert.AreEqual("9.9.9.9", parsed.DnsPolicy.DirectDnsServer); + Assert.AreEqual("149.112.112.112", parsed.DnsPolicy.DirectDnsFallbackServer); + Assert.IsTrue(parsed.DnsPolicy.DirectDnsRouteThroughShadowsocks); + Assert.AreEqual("https://dns.example/dns-query", parsed.DnsPolicy.CustomDohUrl); + Assert.IsTrue(parsed.DnsPolicy.CustomDohRouteThroughShadowsocks); + Assert.AreEqual(38471, parsed.DnsPolicy.DnsCryptPort); + Assert.AreEqual(12345, parsed.DnsPolicy.DnsCryptProcessId); + StringAssert.Contains(parsed.DnsPolicy.DnsCryptComponentRoot, "DNSCryptProxy"); + Assert.IsTrue(parsed.DnsPolicy.FailClosed); + } + + [TestMethod] + public void AdminCaptureRequest_DnsCryptForcesFailClosedEvenForLegacyFalseSetting() + { + Configuration configuration = new() + { + dnsPolicy = new DnsPolicyConfig + { + mode = Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + dnsCrypt = new DnsCryptConfig { failClosed = false }, + }, + }; + + JObject request = JObject.Parse(AdminCaptureManager.BuildStartRequest( + configuration, + @"C:\\WinDivert", + System.Array.Empty(), + DnsCaptureRuntimeState.Unavailable)); + + Assert.IsTrue(request["dnsPolicy"]!["failClosed"]!.Value()); + } + + [TestMethod] + public void AdminCaptureRequest_UnavailableRuntimeSendsZeroEndpointForFailClosed() + { + Configuration configuration = new() + { + dnsPolicy = new DnsPolicyConfig + { + mode = Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + dnsCrypt = new DnsCryptConfig { failClosed = true }, + }, + }; + + JObject request = JObject.Parse(AdminCaptureManager.BuildStartRequest( + configuration, + @"C:\\WinDivert", + System.Array.Empty(), + DnsCaptureRuntimeState.Unavailable)); + + Assert.AreEqual(0, request["dnsPolicy"]!["dnsCryptPort"]!.Value()); + Assert.AreEqual(0, request["dnsPolicy"]!["dnsCryptProcessId"]!.Value()); + Assert.IsTrue(request["dnsPolicy"]!["failClosed"]!.Value()); + } + + [TestMethod] + public void ApplicationPolicy_ManagedDnsCryptExecutable_IsAlwaysDirect() + { + string root = Path.Combine(Path.GetTempPath(), "Shadowsocks", "Components", "DNSCryptProxy"); + StartRequest request = new() + { + MainProcessId = 1, + DefaultRoute = RouteAction.Proxy, + DnsPolicy = new DnsPolicyDto { DnsCryptComponentRoot = root }, + }; + ApplicationPolicy policy = new(request); + string executable = Path.Combine(root, ".prepared-test", "dnscrypt-proxy.exe"); + + Assert.IsTrue(policy.IsExcludedProcess(4321, executable)); + Assert.IsTrue(policy.IsDnsInterceptionExempt(4321, executable)); + Assert.AreEqual(RouteAction.Direct, policy.Evaluate(6, 443, 4321, executable, "dnscrypt-proxy")); + } + + [TestMethod] + public void ApplicationPolicy_DnsCryptNamedExecutableOutsideManagedRoot_IsNotExcluded() + { + string root = Path.Combine(Path.GetTempPath(), "Shadowsocks", "Components", "DNSCryptProxy"); + StartRequest request = new() + { + MainProcessId = 1, + DefaultRoute = RouteAction.Proxy, + DnsPolicy = new DnsPolicyDto { DnsCryptComponentRoot = root }, + }; + ApplicationPolicy policy = new(request); + string executable = Path.Combine(Path.GetTempPath(), "Other", "dnscrypt-proxy.exe"); + + Assert.IsFalse(policy.IsExcludedProcess(4321, executable)); + Assert.IsFalse(policy.IsDnsInterceptionExempt(4321, executable)); + Assert.AreEqual(RouteAction.Proxy, policy.Evaluate(6, 443, 4321, executable, "dnscrypt-proxy")); + } + + [TestMethod] + public void ApplicationPolicy_DnsCryptInterceptsMainAndPluginDnsButExemptsDnsCryptItself() + { + StartRequest request = new() + { + MainProcessId = 100, + ExcludedProcessIds = [200, 300], + DefaultRoute = RouteAction.Proxy, + DnsPolicy = new DnsPolicyDto + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.DnsCrypt, + DnsCryptProcessId = 300, + }, + }; + ApplicationPolicy policy = new(request); + + // They still bypass generic application capture to prevent transport recursion. + Assert.IsTrue(policy.IsExcludedProcess(100, @"C:\Shadowsocks\Shadowsocks.exe")); + Assert.IsTrue(policy.IsExcludedProcess(200, @"C:\Shadowsocks\Plugins\v2ray-plugin.exe")); + + // But UDP/TCP 53 from them must go through DNSCrypt; otherwise they can leak. + Assert.IsFalse(policy.IsDnsInterceptionExempt(100, @"C:\Shadowsocks\Shadowsocks.exe")); + Assert.IsFalse(policy.IsDnsInterceptionExempt(200, @"C:\Shadowsocks\Plugins\v2ray-plugin.exe")); + Assert.IsTrue(policy.IsDnsInterceptionExempt(300, null)); + } + + [TestMethod] + public void DnsRoutingPolicy_FailClosedStillBlocksGenericTrafficExcludedProcess() + { + DnsPolicyDto policy = new() + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.DnsCrypt, + FailClosed = true, + }; + + // A Shadowsocks/plugin traffic exclusion is intentionally NOT passed as a DNS exemption. + Assert.AreEqual(DnsRouteDecision.Block, DnsRoutingPolicy.Evaluate(policy, dnsInterceptionExempt: false)); + } + + [TestMethod] + public void ServiceResponse_CaptureHealthRoundTripsThroughSourceGeneratedJson() + { + ServiceResponse expected = new() + { + Success = true, + CaptureActive = true, + TcpRedirectPort = 31001, + UdpRedirectPort = 31002, + DnsInterceptionActive = true, + }; + + string json = JsonSerializer.Serialize(expected, NetworkServiceJsonContext.Default.ServiceResponse); + ServiceResponse actual = JsonSerializer.Deserialize(json, NetworkServiceJsonContext.Default.ServiceResponse); + + Assert.IsNotNull(actual); + Assert.IsTrue(actual.CaptureActive); + Assert.AreEqual(31001, actual.TcpRedirectPort); + Assert.AreEqual(31002, actual.UdpRedirectPort); + Assert.IsTrue(actual.DnsInterceptionActive); + } + + [TestMethod] + public async Task TransparentDnsRelay_Udp_ForwardsPayloadToDnsCryptLoopback() + { + using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); + using Socket dnsCrypt = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + dnsCrypt.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + int dnsCryptPort = ((IPEndPoint)dnsCrypt.LocalEndPoint!).Port; + + Task echoTask = Task.Run(async () => + { + byte[] echoBuffer = new byte[512]; + EndPoint peer = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult received = await dnsCrypt.ReceiveFromAsync( + echoBuffer, SocketFlags.None, peer, timeout.Token).ConfigureAwait(false); + await dnsCrypt.SendToAsync( + echoBuffer.AsMemory(0, received.ReceivedBytes), + SocketFlags.None, + received.RemoteEndPoint, + timeout.Token).ConfigureAwait(false); + }, timeout.Token); + + FlowRegistry flows = CreateDnsRelayFlowRegistry(); + await using TransparentDnsRelay relay = new(flows, dnsCryptPort); + relay.Start(); + + using Socket application = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + application.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + ushort applicationPort = checked((ushort)((IPEndPoint)application.LocalEndPoint!).Port); + FlowKey key = new(17, IPAddress.Loopback, applicationPort, IPAddress.Loopback, 53); + flows.MarkReflected(new FlowState(key, 77, null, "test", RouteAction.Direct, RouteAction.Direct, DateTime.UtcNow)); + + byte[] payload = [0x12, 0x34, 0x01, 0x00, 0x00, 0x01]; + await application.SendToAsync( + payload, + SocketFlags.None, + new IPEndPoint(IPAddress.Loopback, relay.UdpPort), + timeout.Token).ConfigureAwait(false); + + byte[] response = new byte[512]; + int responseLength = await application.ReceiveAsync(response, SocketFlags.None, timeout.Token).ConfigureAwait(false); + CollectionAssert.AreEqual(payload, response.AsSpan(0, responseLength).ToArray()); + await echoTask.ConfigureAwait(false); + } + + [TestMethod] + public async Task TransparentDnsRelay_Udp_IdleFlowsAreRemovedAndDisposed() + { + using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); + using Socket dnsCrypt = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + dnsCrypt.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + int dnsCryptPort = ((IPEndPoint)dnsCrypt.LocalEndPoint!).Port; + + Task echoTask = Task.Run(async () => + { + byte[] echoBuffer = new byte[512]; + EndPoint peer = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult received = await dnsCrypt.ReceiveFromAsync( + echoBuffer, SocketFlags.None, peer, timeout.Token).ConfigureAwait(false); + await dnsCrypt.SendToAsync( + echoBuffer.AsMemory(0, received.ReceivedBytes), + SocketFlags.None, + received.RemoteEndPoint, + timeout.Token).ConfigureAwait(false); + }, timeout.Token); + + FlowRegistry flows = CreateDnsRelayFlowRegistry(); + await using TransparentDnsRelay relay = new( + flows, + dnsCryptPort, + udpIdleTimeout: TimeSpan.FromMilliseconds(500), + udpSweepInterval: TimeSpan.FromMilliseconds(50)); + relay.Start(); + + using Socket application = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + application.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + ushort applicationPort = checked((ushort)((IPEndPoint)application.LocalEndPoint!).Port); + FlowKey key = new(17, IPAddress.Loopback, applicationPort, IPAddress.Loopback, 53); + flows.MarkReflected(new FlowState(key, 77, null, "test", RouteAction.Direct, RouteAction.Direct, DateTime.UtcNow)); + + byte[] payload = [0x12, 0x34, 0x01, 0x00, 0x00, 0x01]; + await application.SendToAsync( + payload, + SocketFlags.None, + new IPEndPoint(IPAddress.Loopback, relay.UdpPort), + timeout.Token).ConfigureAwait(false); + byte[] response = new byte[512]; + _ = await application.ReceiveAsync(response, SocketFlags.None, timeout.Token).ConfigureAwait(false); + await echoTask.ConfigureAwait(false); + Assert.AreEqual(1, relay.UdpFlowCount); + + DateTime deadline = DateTime.UtcNow.AddSeconds(2); + while (relay.UdpFlowCount != 0 && DateTime.UtcNow < deadline) + { + await Task.Delay(25, timeout.Token).ConfigureAwait(false); + } + + Assert.AreEqual(0, relay.UdpFlowCount, "Idle UDP DNS flow must be removed instead of living until Admin Mode stops."); + } + + [TestMethod] + public async Task TransparentDnsRelay_Tcp_ForwardsDnsStreamToDnsCryptLoopback() + { + using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); + using Socket dnsCryptListener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + dnsCryptListener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + dnsCryptListener.Listen(1); + int dnsCryptPort = ((IPEndPoint)dnsCryptListener.LocalEndPoint!).Port; + + Task echoTask = Task.Run(async () => + { + using Socket accepted = await dnsCryptListener.AcceptAsync(timeout.Token).ConfigureAwait(false); + byte[] echoBuffer = new byte[6]; + int offset = 0; + while (offset < echoBuffer.Length) + { + int received = await accepted.ReceiveAsync(echoBuffer.AsMemory(offset), SocketFlags.None, timeout.Token).ConfigureAwait(false); + if (received == 0) + throw new IOException("Test DNSCrypt TCP peer closed before receiving the framed DNS message."); + offset += received; + } + await accepted.SendAsync(echoBuffer, SocketFlags.None, timeout.Token).ConfigureAwait(false); + }, timeout.Token); + + FlowRegistry flows = CreateDnsRelayFlowRegistry(); + await using TransparentDnsRelay relay = new(flows, dnsCryptPort); + relay.Start(); + + using Socket application = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + application.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + ushort applicationPort = checked((ushort)((IPEndPoint)application.LocalEndPoint!).Port); + FlowKey key = new(6, IPAddress.Loopback, applicationPort, IPAddress.Loopback, 53); + flows.MarkReflected(new FlowState(key, 77, null, "test", RouteAction.Direct, RouteAction.Direct, DateTime.UtcNow)); + + await application.ConnectAsync(new IPEndPoint(IPAddress.Loopback, relay.TcpPort), timeout.Token).ConfigureAwait(false); + byte[] payload = [0x00, 0x04, 0x12, 0x34, 0x01, 0x00]; + await application.SendAsync(payload, SocketFlags.None, timeout.Token).ConfigureAwait(false); + byte[] response = new byte[payload.Length]; + int offset = 0; + while (offset < response.Length) + { + int received = await application.ReceiveAsync(response.AsMemory(offset), SocketFlags.None, timeout.Token).ConfigureAwait(false); + Assert.AreNotEqual(0, received); + offset += received; + } + + CollectionAssert.AreEqual(payload, response); + await echoTask.ConfigureAwait(false); + } + + [TestMethod] + public void WinDivertReceiveTerminationIsOnlyExpectedDuringShutdown() + { + Assert.IsTrue(WinDivertTransparentRouter.IsExpectedReceiveTermination(shutdownRequested: true, errorCode: 6)); + Assert.IsTrue(WinDivertTransparentRouter.IsExpectedReceiveTermination(shutdownRequested: true, errorCode: 232)); + Assert.IsTrue(WinDivertTransparentRouter.IsExpectedReceiveTermination(shutdownRequested: true, errorCode: 995)); + Assert.IsFalse(WinDivertTransparentRouter.IsExpectedReceiveTermination(shutdownRequested: false, errorCode: 995)); + Assert.IsFalse(WinDivertTransparentRouter.IsExpectedReceiveTermination(shutdownRequested: true, errorCode: 5)); + } + + [TestMethod] + public void WinDivertSendValidationRejectsNativeFailureAndShortWrite() + { + System.ComponentModel.Win32Exception nativeFailure = Assert.ThrowsExactly(() => + WinDivertTransparentRouter.ValidateSendResult( + succeeded: false, + sentLength: 0, + expectedLength: 128, + errorCode: 5)); + Assert.AreEqual(5, nativeFailure.NativeErrorCode); + + Assert.ThrowsExactly(() => + WinDivertTransparentRouter.ValidateSendResult( + succeeded: true, + sentLength: 64, + expectedLength: 128, + errorCode: 0)); + } + + + [TestMethod] + public async Task TransparentDnsRelay_StartTwiceIsRejectedAndDisposeIsIdempotent() + { + using Socket dnsCrypt = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + dnsCrypt.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + int dnsCryptPort = ((IPEndPoint)dnsCrypt.LocalEndPoint!).Port; + + FlowRegistry flows = CreateDnsRelayFlowRegistry(); + TransparentDnsRelay relay = new(flows, dnsCryptPort); + relay.Start(); + Assert.ThrowsExactly(() => relay.Start()); + + await relay.DisposeAsync(); + await relay.DisposeAsync(); + } + + [TestMethod] + public void FlowRegistry_PeerIndexPreservesMultiHomedFallbackWithoutLinearScan() + { + FlowRegistry flows = CreateDnsRelayFlowRegistry(); + DateTime now = DateTime.UtcNow; + FlowKey firstKey = new( + 17, + IPAddress.Parse("192.0.2.10"), + 50123, + IPAddress.Parse("9.9.9.9"), + 53); + FlowKey secondKey = firstKey with { LocalAddress = IPAddress.Parse("192.0.2.11") }; + FlowState first = new(firstKey, 77, null, "test-1", RouteAction.Direct, RouteAction.Direct, now.AddSeconds(-1)); + FlowState second = new(secondKey, 78, null, "test-2", RouteAction.Direct, RouteAction.Direct, now); + flows.MarkReflected(first); + flows.MarkReflected(second); + + Assert.IsTrue(flows.TryGetReflected( + 17, + firstKey.LocalAddress, + 50123, + IPAddress.Parse("9.9.9.9"), + out FlowState exactFirst)); + Assert.AreSame(first, exactFirst); + + Assert.IsTrue(flows.TryGetReflected( + 17, + secondKey.LocalAddress, + 50123, + IPAddress.Parse("9.9.9.9"), + out FlowState exactSecond)); + Assert.AreSame(second, exactSecond); + + Assert.IsTrue(flows.TryGetByPeer(17, IPAddress.Parse("9.9.9.9"), 50123, out FlowState byPeer)); + Assert.AreSame(second, byPeer); + + Assert.IsTrue(flows.TryGetReflected( + 17, + IPAddress.Parse("198.51.100.20"), + 50123, + IPAddress.Parse("9.9.9.9"), + out FlowState fallback)); + Assert.AreSame(second, fallback); + } + + [TestMethod] + public async Task CaptureChildSupervisor_InitialPingIsImmediateAndVersioned() + { + await using CaptureChildSupervisor supervisor = new(); + System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + ServiceResponse response = await supervisor.PingAsync(); + + stopwatch.Stop(); + Assert.IsTrue(response.Success); + Assert.IsFalse(response.CaptureActive); + Assert.AreEqual("pong", response.Message); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.Version)); + Assert.IsTrue(stopwatch.Elapsed < TimeSpan.FromSeconds(1)); + } + + private static FlowRegistry CreateDnsRelayFlowRegistry() + { + StartRequest request = new() + { + MainProcessId = -1, + DefaultRoute = RouteAction.Direct, + }; + return new FlowRegistry(new ApplicationPolicy(request)); + } + + private static DnsPolicyDto ReadyDnsCryptPolicy() => new() + { + Mode = Shadowsocks.NetworkService.Ipc.DnsPolicyMode.DnsCrypt, + DnsCryptPort = 5300, + DnsCryptProcessId = 42, + FailClosed = true, + }; +} diff --git a/Shadowsocks.UnitTests/DnsCryptBootstrapPolicyTests.cs b/Shadowsocks.UnitTests/DnsCryptBootstrapPolicyTests.cs new file mode 100644 index 000000000..0c36738cd --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptBootstrapPolicyTests.cs @@ -0,0 +1,71 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptBootstrapPolicyTests + { + [TestMethod] + public void DirectDnsCryptDoesNotRestrictShadowsocksHostnames() + { + Configuration configuration = CreateConfiguration("example.org"); + configuration.dnsPolicy.dnsCrypt.routeThroughShadowsocks = false; + + Assert.AreEqual(0, DnsCryptBootstrapPolicy.GetUnsafeEndpoints(configuration).Count); + } + + [TestMethod] + public void RoutedDnsCryptRequiresIpLiteralShadowsocksEndpoints() + { + Configuration configuration = CreateConfiguration("ss.example.org"); + + IReadOnlyList unsafeEndpoints = DnsCryptBootstrapPolicy.GetUnsafeEndpoints(configuration); + + CollectionAssert.AreEqual(new[] { "ss.example.org" }, unsafeEndpoints.ToArray()); + Assert.ThrowsExactly(() => DnsCryptBootstrapPolicy.Validate(configuration)); + } + + [TestMethod] + public void RoutedDnsCryptAcceptsIpv4AndIpv6ServerEndpoints() + { + Configuration configuration = CreateConfiguration("203.0.113.7"); + configuration.configs.Add(new Server { server = "2001:db8::7", ServerPort = 8388, password = "test" }); + + DnsCryptBootstrapPolicy.Validate(configuration); + Assert.AreEqual(0, DnsCryptBootstrapPolicy.GetUnsafeEndpoints(configuration).Count); + } + + [TestMethod] + public void RoutedDnsCryptAlsoRequiresIpLiteralForwardProxyEndpoint() + { + Configuration configuration = CreateConfiguration("203.0.113.7"); + configuration.proxy.useProxy = true; + configuration.proxy.proxyServer = "proxy.example.org"; + configuration.proxy.proxyPort = 1080; + + IReadOnlyList unsafeEndpoints = DnsCryptBootstrapPolicy.GetUnsafeEndpoints(configuration); + + CollectionAssert.Contains(unsafeEndpoints.ToArray(), "proxy.example.org"); + } + + private static Configuration CreateConfiguration(string serverHost) + { + Configuration configuration = new() + { + configs = [new Server { server = serverHost, ServerPort = 8388, password = "test" }], + dnsPolicy = new DnsPolicyConfig + { + mode = DnsPolicyMode.DnsCrypt, + dnsCrypt = new DnsCryptConfig { routeThroughShadowsocks = true }, + }, + }; + return configuration; + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptCleanModeIntegrationTests.cs b/Shadowsocks.UnitTests/DnsCryptCleanModeIntegrationTests.cs new file mode 100644 index 000000000..afd2c9afc --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptCleanModeIntegrationTests.cs @@ -0,0 +1,134 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptCleanModeIntegrationTests + { + [TestMethod] + public async Task ComponentAndRuntimeStayInsideProvidedCleanSessionRoot() + { + string cleanRoot = Path.Combine( + Path.GetTempPath(), + "Shadowsocks.UnitTests", + "Clean", + Guid.NewGuid().ToString("N")); + string componentRoot = Path.Combine(cleanRoot, "Components", "DNSCryptProxy"); + string updateRoot = Path.Combine(cleanRoot, "Temp", "Updates", "DNSCryptProxy"); + string runtimeRoot = Path.Combine(cleanRoot, "Runtime", "DNSCryptProxy"); + Directory.CreateDirectory(Path.Combine(componentRoot, "2.1.18")); + string executable = Path.Combine(componentRoot, "2.1.18", "dnscrypt-proxy.exe"); + await File.WriteAllBytesAsync(executable, [0x4D, 0x5A]); + + using var httpClient = new HttpClient(new RejectNetworkHandler()); + using var component = new DnsCryptComponentManager( + httpClient, + componentRoot, + updateRoot, + [DnsCryptComponentManager.ReleaseSigningPublicKey]); + component.ActivateVersion(new Version(2, 1, 18)); + component.SetAutoUpdate(false); + + var platform = new FakeRuntimePlatform(); + using var runtime = new DnsCryptRuntimeManager( + component.GetStatus, + runtimeRoot, + platform, + () => 55331, + (_, _, _) => Task.FromResult(true), + [TimeSpan.Zero], + TimeSpan.FromSeconds(1)); + + DnsCryptRuntimeStatus started = await runtime.StartAsync( + new DnsCryptRuntimeStartOptions(new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = false, + serverNames = new System.Collections.Generic.List { "test-resolver" }, + }) + { + StaticResolverStamps = new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["test-resolver"] = "sdns://test", + }, + }); + Assert.IsTrue(started.IsRunning); + string configPath = started.ConfigPath + ?? throw new AssertFailedException("Running DNSCrypt status must expose its generated configuration path."); + Assert.IsTrue(configPath.StartsWith(cleanRoot, StringComparison.OrdinalIgnoreCase)); + + foreach (string path in Directory.EnumerateFileSystemEntries(cleanRoot, "*", SearchOption.AllDirectories)) + { + Assert.IsTrue( + Path.GetFullPath(path).StartsWith(Path.GetFullPath(cleanRoot), StringComparison.OrdinalIgnoreCase), + path); + } + + Assert.IsTrue(File.Exists(Path.Combine(componentRoot, "component.json"))); + Assert.IsTrue(File.Exists(Path.Combine(runtimeRoot, "dnscrypt-proxy.toml"))); + await runtime.StopAsync(); + try { Directory.Delete(cleanRoot, recursive: true); } catch { } + } + + private sealed class RejectNetworkHandler : HttpMessageHandler + { + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new AssertFailedException("Clean Mode integration test must not use the network."); + } + } + + private sealed class FakeRuntimePlatform : IDnsCryptRuntimePlatform + { + public Task ExecuteAsync( + string executablePath, + string workingDirectory, + System.Collections.Generic.IReadOnlyList arguments, + TimeSpan timeout, + CancellationToken cancellationToken) + { + string output = arguments.Contains("-version") ? "2.1.18" : "Configuration successfully checked"; + return Task.FromResult(new DnsCryptCommandResult(0, output, string.Empty)); + } + + public IDnsCryptRunningProcess Start( + string executablePath, + string workingDirectory, + System.Collections.Generic.IReadOnlyList arguments, + Action onStandardOutput, + Action onStandardError) + { + return new FakeRunningProcess(); + } + } + + private sealed class FakeRunningProcess : IDnsCryptRunningProcess + { + public int Id => 4242; + public bool HasExited { get; private set; } + public event EventHandler? Exited; + + public void Kill() + { + if (HasExited) + return; + HasExited = true; + Exited?.Invoke(this, EventArgs.Empty); + } + + public Task WaitForExitAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public void Dispose() { } + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptComponentManagerTests.cs b/Shadowsocks.UnitTests/DnsCryptComponentManagerTests.cs new file mode 100644 index 000000000..9caf67224 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptComponentManagerTests.cs @@ -0,0 +1,752 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Shadowsocks.Controller.Service; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + [DoNotParallelize] + public class DnsCryptComponentManagerTests + { + [TestMethod] + public void SecurityPinsMatchOfficialDnsCryptProject() + { + Assert.AreEqual("DNSCrypt/dnscrypt-proxy", ReadDnsCryptConstant(nameof(DnsCryptComponentManager.Repository))); + Assert.AreEqual("dnscrypt-proxy.exe", ReadDnsCryptConstant(nameof(DnsCryptComponentManager.ExpectedExecutableName))); + + byte[] minisignKey = Convert.FromBase64String(DnsCryptComponentManager.ReleaseSigningPublicKey); + Assert.AreEqual(42, minisignKey.Length); + } + + private static string ReadDnsCryptConstant(string fieldName) + { + System.Reflection.FieldInfo field = typeof(DnsCryptComponentManager).GetField( + fieldName, + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + Assert.IsNotNull(field); + return (string)field.GetRawConstantValue(); + } + + private string root; + private string componentRoot; + private string updateRoot; + + [TestInitialize] + public void Initialize() + { + root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", "DnsCrypt", Guid.NewGuid().ToString("N")); + componentRoot = Path.Combine(root, "Components", "DNSCryptProxy"); + updateRoot = Path.Combine(root, "Temp", "Updates", "DNSCryptProxy"); + Directory.CreateDirectory(root); + } + + [TestCleanup] + public void Cleanup() + { + try { Directory.Delete(root, recursive: true); } catch { } + } + + [TestMethod] + public void ReleaseSelectionAcceptsExactWindowsX64ArchiveAndSignature() + { + DnsCryptComponentManager.GitHubRelease release = CreateRelease( + "2.1.18", + CreateAsset("dnscrypt-proxy-win64-2.1.18.zip", 1024, new string('a', 64)), + CreateAsset("dnscrypt-proxy-win64-2.1.18.zip.minisig", 512)); + + DnsCryptReleaseInfo selected = DnsCryptComponentManager.SelectRelease(release); + + Assert.AreEqual(new Version(2, 1, 18), selected.Version); + Assert.AreEqual("dnscrypt-proxy-win64-2.1.18.zip", selected.ArchiveName); + Assert.AreEqual("dnscrypt-proxy-win64-2.1.18.zip.minisig", selected.SignatureName); + Assert.AreEqual(new string('a', 64), selected.Sha256Digest); + } + + [TestMethod] + public void ReleaseSelectionAcceptsVPrefixedTagForUpstreamCompatibility() + { + DnsCryptComponentManager.GitHubAsset archive = CreateAsset("dnscrypt-proxy-win64-2.1.18.zip", 1024); + archive.DownloadUrl = OfficialAssetUrl("v2.1.18", archive.Name); + DnsCryptComponentManager.GitHubAsset signature = CreateAsset("dnscrypt-proxy-win64-2.1.18.zip.minisig", 512); + signature.DownloadUrl = OfficialAssetUrl("v2.1.18", signature.Name); + DnsCryptComponentManager.GitHubRelease release = CreateRelease("v2.1.18", archive, signature); + + DnsCryptReleaseInfo selected = DnsCryptComponentManager.SelectRelease(release); + + Assert.AreEqual(new Version(2, 1, 18), selected.Version); + } + + [TestMethod] + public void ReleaseSelectionRejectsWrongArchitecture() + { + DnsCryptComponentManager.GitHubRelease release = CreateRelease( + "2.1.18", + CreateAsset("dnscrypt-proxy-win32-2.1.18.zip", 1024), + CreateAsset("dnscrypt-proxy-win32-2.1.18.zip.minisig", 512)); + + Assert.ThrowsExactly(() => DnsCryptComponentManager.SelectRelease(release)); + } + + [TestMethod] + public void ReleaseSelectionRejectsAssetVersionDifferentFromTag() + { + DnsCryptComponentManager.GitHubRelease release = CreateRelease( + "2.1.18", + CreateAsset("dnscrypt-proxy-win64-2.1.17.zip", 1024), + CreateAsset("dnscrypt-proxy-win64-2.1.17.zip.minisig", 512)); + + Assert.ThrowsExactly(() => DnsCryptComponentManager.SelectRelease(release)); + } + + [TestMethod] + public void ReleaseSelectionRequiresMinisignAsset() + { + DnsCryptComponentManager.GitHubRelease release = CreateRelease( + "2.1.18", + CreateAsset("dnscrypt-proxy-win64-2.1.18.zip", 1024)); + + Assert.ThrowsExactly(() => DnsCryptComponentManager.SelectRelease(release)); + } + + [TestMethod] + public void ReleaseSelectionRejectsDownloadOutsideOfficialRepository() + { + DnsCryptComponentManager.GitHubAsset archive = CreateAsset("dnscrypt-proxy-win64-2.1.18.zip", 1024); + archive.DownloadUrl = "https://example.com/dnscrypt-proxy-win64-2.1.18.zip"; + DnsCryptComponentManager.GitHubRelease release = CreateRelease( + "2.1.18", + archive, + CreateAsset("dnscrypt-proxy-win64-2.1.18.zip.minisig", 512)); + + Assert.ThrowsExactly(() => DnsCryptComponentManager.SelectRelease(release)); + } + + [TestMethod] + public void SafeZipExtractionAcceptsNormalEntries() + { + string zip = CreateZipFile( + "normal.zip", + ("dnscrypt-proxy/dnscrypt-proxy.exe", new byte[] { 0x4D, 0x5A }), + ("dnscrypt-proxy/LICENSE", Encoding.UTF8.GetBytes("ISC"))); + string destination = Path.Combine(root, "extract-normal"); + + DnsCryptComponentManager.ExtractZipSafely(zip, destination); + + Assert.IsTrue(File.Exists(Path.Combine(destination, "dnscrypt-proxy", "dnscrypt-proxy.exe"))); + } + + [TestMethod] + public void SafeZipExtractionRejectsParentTraversal() + { + string zip = CreateZipFile("traversal.zip", ("../outside.exe", new byte[] { 1, 2, 3 })); + string destination = Path.Combine(root, "extract-traversal"); + + Assert.ThrowsExactly(() => + DnsCryptComponentManager.ExtractZipSafely(zip, destination)); + Assert.IsFalse(File.Exists(Path.Combine(root, "outside.exe"))); + } + + [TestMethod] + public void SafeZipExtractionRejectsWindowsAbsolutePath() + { + string zip = CreateZipFile("absolute.zip", ("C:/Windows/evil.exe", new byte[] { 1 })); + + Assert.ThrowsExactly(() => + DnsCryptComponentManager.ExtractZipSafely(zip, Path.Combine(root, "extract-absolute"))); + } + + [TestMethod] + public void SafeZipExtractionRejectsUnixSymlinkEntry() + { + string zip = Path.Combine(root, "symlink.zip"); + using (FileStream stream = new(zip, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create)) + { + ZipArchiveEntry entry = archive.CreateEntry("dnscrypt-proxy.exe"); + entry.ExternalAttributes = unchecked((int)(0xA000 << 16)); + using Stream output = entry.Open(); + output.WriteByte(0); + } + + Assert.ThrowsExactly(() => + DnsCryptComponentManager.ExtractZipSafely(zip, Path.Combine(root, "extract-symlink"))); + } + + [TestMethod] + public void MinisignVerifierAcceptsValidModernSignatureAndRejectsTampering() + { + byte[] payload = Encoding.UTF8.GetBytes("dnscrypt-proxy archive fixture"); + string signedFile = Path.Combine(root, "signed.zip"); + File.WriteAllBytes(signedFile, payload); + MinisignFixture fixture = CreateMinisignFixture(payload); + + Assert.IsTrue(MinisignVerifier.VerifyFile(signedFile, fixture.SignatureText, fixture.PublicKey)); + + File.AppendAllText(signedFile, "tampered", Encoding.UTF8); + Assert.IsFalse(MinisignVerifier.VerifyFile(signedFile, fixture.SignatureText, fixture.PublicKey)); + } + + [TestMethod] + public void MinisignVerifierAcceptsLegacyResolverCatalogSignatureAndRejectsTampering() + { + byte[] payload = Encoding.UTF8.GetBytes("dnscrypt resolver catalog fixture"); + string signedFile = Path.Combine(root, "public-resolvers.md"); + File.WriteAllBytes(signedFile, payload); + MinisignFixture fixture = CreateMinisignFixture(payload, prehashed: false); + + Assert.IsTrue(MinisignVerifier.VerifyFileAllowLegacy(signedFile, fixture.SignatureText, fixture.PublicKey)); + + File.AppendAllText(signedFile, "tampered", Encoding.UTF8); + Assert.IsFalse(MinisignVerifier.VerifyFileAllowLegacy(signedFile, fixture.SignatureText, fixture.PublicKey)); + } + + [TestMethod] + public void MinisignVerifierRecognizesOfficialResolverLegacySignatureFormat() + { + string signedFile = Path.Combine(root, "official-resolver-format.md"); + File.WriteAllText(signedFile, "intentionally not the signed upstream catalog", Encoding.UTF8); + string signatureText = string.Join("\n", new[] + { + "untrusted comment: signature from minisign secret key", + "RWQf6LRCGA9i59dR/JthmdbOIHVSepvwImbIu8RNVy4drRsi1YPHp5bLTqvDVu3BgNy7/eYDZWDnKzxl+aovrP1VNU6qBzD9CQs=", + "trusted comment: timestamp:1786712947\tfile:public-resolvers.md", + "zyX1ZY3vlnMtEY4vditqPW3+XnMcqvgVWXa7FPvWv0VDcVXs1d2rIXtBpj2bZO8xHQUIqlotHNlmvdi9F2GMBA==", + string.Empty, + }); + + Assert.IsFalse(MinisignVerifier.VerifyFileAllowLegacy( + signedFile, + signatureText, + DnsCryptTomlGenerator.PublicResolversMinisignKey)); + } + + [TestMethod] + public void MinisignVerifierDefaultPathRejectsLegacySignature() + { + byte[] payload = Encoding.UTF8.GetBytes("legacy release fixture"); + string signedFile = Path.Combine(root, "legacy-release.zip"); + File.WriteAllBytes(signedFile, payload); + MinisignFixture fixture = CreateMinisignFixture(payload, prehashed: false); + + Assert.ThrowsExactly(() => + MinisignVerifier.VerifyFile(signedFile, fixture.SignatureText, fixture.PublicKey)); + } + + [TestMethod] + public void MinisignVerifierRejectsTamperedTrustedCommentForLegacySignature() + { + byte[] payload = Encoding.UTF8.GetBytes("dnscrypt resolver catalog fixture"); + string signedFile = Path.Combine(root, "public-resolvers-comment.md"); + File.WriteAllBytes(signedFile, payload); + MinisignFixture fixture = CreateMinisignFixture(payload, prehashed: false); + string tamperedSignature = fixture.SignatureText.Replace( + "file:public-resolvers.md", + "file:tampered-resolvers.md", + StringComparison.Ordinal); + + Assert.IsFalse(MinisignVerifier.VerifyFileAllowLegacy(signedFile, tamperedSignature, fixture.PublicKey)); + } + + [TestMethod] + public void MinisignVerifierRejectsDifferentTrustedKey() + { + byte[] payload = Encoding.UTF8.GetBytes("signed content"); + string signedFile = Path.Combine(root, "wrong-key.zip"); + File.WriteAllBytes(signedFile, payload); + MinisignFixture signature = CreateMinisignFixture(payload, seedOffset: 0); + MinisignFixture otherKey = CreateMinisignFixture(payload, seedOffset: 33); + + Assert.IsFalse(MinisignVerifier.VerifyFile(signedFile, signature.SignatureText, otherKey.PublicKey)); + } + + [TestMethod] + public async Task PrepareLatestDownloadsAndVerifiesOfficialArchiveWithoutActivation() + { + byte[] executable = { 0x4D, 0x5A, 0x90, 0x00 }; + byte[] archive = CreateZipBytes(("dnscrypt-proxy-win64/dnscrypt-proxy.exe", executable)); + MinisignFixture signature = CreateMinisignFixture(archive); + string digest = Convert.ToHexString(SHA256.HashData(archive)).ToLowerInvariant(); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length, digest, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + + DnsCryptPreparedComponent installed = await manager.PrepareLatestAsync(); + DnsCryptComponentStatus status = manager.GetStatus(); + + Assert.AreEqual(new Version(2, 1, 18), installed.Version); + Assert.IsTrue(File.Exists(installed.ExecutablePath)); + CollectionAssert.AreEqual(executable, File.ReadAllBytes(installed.ExecutablePath)); + Assert.IsFalse(status.IsInstalled); + Assert.IsNull(status.ActiveVersion); + Assert.IsTrue(status.AutoUpdate); + Assert.IsFalse(File.Exists(Path.Combine(componentRoot, "component.json")), "Prepare must not persist active metadata before validation/activation."); + Assert.IsFalse(Directory.Exists(updateRoot) && Directory.EnumerateFileSystemEntries(updateRoot).Any()); + } + + [TestMethod] + public async Task PrepareLatestDoesNotChangeActiveVersionBeforeActivation() + { + byte[] archive = CreateZipBytes(("dnscrypt-proxy.exe", new byte[] { 0x4D, 0x5A })); + MinisignFixture signature = CreateMinisignFixture(archive); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length, null, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + + DnsCryptPreparedComponent prepared = await manager.PrepareLatestAsync(); + + Assert.IsTrue(File.Exists(prepared.ExecutablePath)); + Assert.IsFalse(manager.GetStatus().IsInstalled); + + DnsCryptActivationLease activation = manager.ActivatePrepared(prepared); + manager.CommitActivation(activation); + Assert.IsTrue(manager.GetStatus().IsInstalled); + } + + [TestMethod] + public async Task ForcePrepareOfActiveVersionKeepsOldExecutableUntilActivation() + { + byte[] oldExecutable = { 0x4D, 0x5A, 0x01 }; + byte[] newExecutable = { 0x4D, 0x5A, 0x02 }; + CreatePreparedVersion("2.1.18", oldExecutable); + + byte[] archive = CreateZipBytes(("dnscrypt-proxy.exe", newExecutable)); + MinisignFixture signature = CreateMinisignFixture(archive); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length, null, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + manager.ActivateVersion(new Version(2, 1, 18)); + string activeExecutable = manager.GetStatus().ExecutablePath; + + DnsCryptPreparedComponent prepared = await manager.PrepareLatestAsync(forceDownload: true); + + Assert.AreNotEqual(Path.GetFullPath(activeExecutable), Path.GetFullPath(prepared.ExecutablePath)); + CollectionAssert.AreEqual(oldExecutable, File.ReadAllBytes(activeExecutable), "Prepare must not replace the active executable."); + CollectionAssert.AreEqual(newExecutable, File.ReadAllBytes(prepared.ExecutablePath)); + + DnsCryptActivationLease activation = manager.ActivatePrepared(prepared); + + Assert.IsNotNull(activation); + Assert.IsTrue(Directory.Exists(activation.BackupDirectory)); + CollectionAssert.AreEqual(newExecutable, File.ReadAllBytes(manager.GetStatus().ExecutablePath)); + + manager.CommitActivation(activation); + Assert.IsFalse(Directory.Exists(activation.BackupDirectory)); + } + + [TestMethod] + public async Task SameVersionActivationCanRollbackUntilPostActivationCommit() + { + byte[] oldExecutable = { 0x4D, 0x5A, 0x11 }; + byte[] newExecutable = { 0x4D, 0x5A, 0x22 }; + CreatePreparedVersion("2.1.18", oldExecutable); + + byte[] archive = CreateZipBytes(("dnscrypt-proxy.exe", newExecutable)); + MinisignFixture signature = CreateMinisignFixture(archive); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length, null, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + manager.ActivateVersion(new Version(2, 1, 18)); + + DnsCryptPreparedComponent prepared = await manager.PrepareLatestAsync(forceDownload: true); + DnsCryptActivationLease activation = manager.ActivatePrepared(prepared); + + Assert.IsNotNull(activation); + CollectionAssert.AreEqual(newExecutable, File.ReadAllBytes(manager.GetStatus().ExecutablePath)); + Assert.IsTrue(Directory.Exists(activation.BackupDirectory)); + + manager.RollbackActivation(activation); + + CollectionAssert.AreEqual(oldExecutable, File.ReadAllBytes(manager.GetStatus().ExecutablePath)); + Assert.AreEqual(new Version(2, 1, 18), manager.GetStatus().ActiveVersion); + Assert.IsFalse(Directory.Exists(activation.BackupDirectory)); + } + + [TestMethod] + public async Task PrepareLatestRejectsReportedArchiveSizeMismatch() + { + byte[] archive = CreateZipBytes(("dnscrypt-proxy.exe", new byte[] { 1, 2, 3 })); + MinisignFixture signature = CreateMinisignFixture(archive); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length + 1, null, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + + await Assert.ThrowsExactlyAsync(() => manager.PrepareLatestAsync()); + + Assert.IsFalse(manager.GetStatus().IsInstalled); + } + + [TestMethod] + public async Task PrepareLatestRejectsSha256MismatchBeforeInstallation() + { + byte[] archive = CreateZipBytes(("dnscrypt-proxy.exe", new byte[] { 1, 2, 3 })); + MinisignFixture signature = CreateMinisignFixture(archive); + string archiveName = "dnscrypt-proxy-win64-2.1.18.zip"; + string signatureName = archiveName + ".minisig"; + string archiveUrl = OfficialAssetUrl("2.1.18", archiveName); + string signatureUrl = OfficialAssetUrl("2.1.18", signatureName); + string wrongDigest = new string('0', 64); + string releaseJson = CreateReleaseJson("2.1.18", archiveName, archiveUrl, archive.Length, wrongDigest, signatureName, signatureUrl); + + using HttpClient client = CreateHttpClient(new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"] = () => JsonResponse(releaseJson), + [archiveUrl] = () => BytesResponse(archive), + [signatureUrl] = () => BytesResponse(Encoding.UTF8.GetBytes(signature.SignatureText)), + }); + using var manager = new DnsCryptComponentManager(client, componentRoot, updateRoot, [signature.PublicKey]); + + await Assert.ThrowsExactlyAsync(() => manager.PrepareLatestAsync()); + + Assert.IsFalse(manager.GetStatus().IsInstalled); + Assert.IsFalse(Directory.Exists(componentRoot) && Directory.EnumerateDirectories(componentRoot).Any()); + } + + [TestMethod] + public void ActivationKeepsOnePreviousVersionAndRollbackSwapsThem() + { + using var manager = new DnsCryptComponentManager(new HttpClient(new FakeHttpMessageHandler(new Dictionary>())), componentRoot, updateRoot, [DnsCryptComponentManager.ReleaseSigningPublicKey]); + CreatePreparedVersion("2.1.16", new byte[] { 16 }); + CreatePreparedVersion("2.1.17", new byte[] { 17 }); + + manager.ActivateVersion(new Version(2, 1, 17)); + CreatePreparedVersion("2.1.18", new byte[] { 18 }); + manager.ActivateVersion(new Version(2, 1, 18)); + + DnsCryptComponentStatus before = manager.GetStatus(); + Assert.AreEqual(new Version(2, 1, 18), before.ActiveVersion); + Assert.AreEqual(new Version(2, 1, 17), before.PreviousVersion); + Assert.IsFalse(Directory.Exists(Path.Combine(componentRoot, "2.1.16"))); + + DnsCryptPreparedComponent rolledBack = manager.Rollback(); + DnsCryptComponentStatus after = manager.GetStatus(); + Assert.AreEqual(new Version(2, 1, 17), rolledBack.Version); + Assert.AreEqual(new Version(2, 1, 17), after.ActiveVersion); + Assert.AreEqual(new Version(2, 1, 18), after.PreviousVersion); + } + + [TestMethod] + public void DiscardPreparedVersionNeverDeletesActiveOrPreviousVersions() + { + using var manager = new DnsCryptComponentManager(new HttpClient(new FakeHttpMessageHandler(new Dictionary>())), componentRoot, updateRoot, [DnsCryptComponentManager.ReleaseSigningPublicKey]); + CreatePreparedVersion("2.1.17", new byte[] { 17 }); + manager.ActivateVersion(new Version(2, 1, 17)); + CreatePreparedVersion("2.1.18", new byte[] { 18 }); + manager.ActivateVersion(new Version(2, 1, 18)); + CreatePreparedVersion("2.1.19", new byte[] { 19 }); + + manager.DiscardPreparedVersion(new Version(2, 1, 17)); + manager.DiscardPreparedVersion(new Version(2, 1, 18)); + manager.DiscardPreparedVersion(new Version(2, 1, 19)); + + Assert.IsTrue(Directory.Exists(Path.Combine(componentRoot, "2.1.17"))); + Assert.IsTrue(Directory.Exists(Path.Combine(componentRoot, "2.1.18"))); + Assert.IsFalse(Directory.Exists(Path.Combine(componentRoot, "2.1.19"))); + } + + [TestMethod] + public void RemoveDeletesComponentAndUpdateState() + { + using var manager = new DnsCryptComponentManager(new HttpClient(new FakeHttpMessageHandler(new Dictionary>())), componentRoot, updateRoot, [DnsCryptComponentManager.ReleaseSigningPublicKey]); + CreatePreparedVersion("2.1.18", new byte[] { 18 }); + Directory.CreateDirectory(updateRoot); + File.WriteAllText(Path.Combine(updateRoot, "temporary"), "x"); + manager.ActivateVersion(new Version(2, 1, 18)); + + manager.Remove(); + + Assert.IsFalse(Directory.Exists(componentRoot)); + Assert.IsFalse(Directory.Exists(updateRoot)); + Assert.IsFalse(manager.GetStatus().IsInstalled); + } + + [TestMethod] + public void ComponentMetadataPersistsAutoUpdateAndLastCheck() + { + DateTimeOffset checkedAt = new(2026, 8, 18, 12, 30, 0, TimeSpan.Zero); + using (var manager = new DnsCryptComponentManager(new HttpClient(new FakeHttpMessageHandler(new Dictionary>())), componentRoot, updateRoot, [DnsCryptComponentManager.ReleaseSigningPublicKey])) + { + manager.SetAutoUpdate(false); + manager.RecordUpdateCheck(checkedAt); + } + + using var reloaded = new DnsCryptComponentManager(new HttpClient(new FakeHttpMessageHandler(new Dictionary>())), componentRoot, updateRoot, [DnsCryptComponentManager.ReleaseSigningPublicKey]); + DnsCryptComponentStatus status = reloaded.GetStatus(); + + Assert.IsFalse(status.AutoUpdate); + Assert.AreEqual(checkedAt, status.LastUpdateCheckUtc); + } + + private void CreatePreparedVersion(string version, byte[] content) + { + string directory = Path.Combine(componentRoot, version); + Directory.CreateDirectory(directory); + File.WriteAllBytes(Path.Combine(directory, DnsCryptComponentManager.ExpectedExecutableName), content); + } + + private static DnsCryptComponentManager.GitHubRelease CreateRelease( + string tag, + params DnsCryptComponentManager.GitHubAsset[] assets) => new() + { + TagName = tag, + Draft = false, + Prerelease = false, + Assets = assets, + }; + + private static DnsCryptComponentManager.GitHubAsset CreateAsset(string name, long size, string digest = null) => new() + { + Name = name, + Size = size, + Digest = digest is null ? null : "sha256:" + digest, + DownloadUrl = OfficialAssetUrl("2.1.18", name), + }; + + private static string OfficialAssetUrl(string version, string name) => + $"https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/{version}/{name}"; + + private string CreateZipFile(string name, params (string Name, byte[] Content)[] entries) + { + string path = Path.Combine(root, name); + File.WriteAllBytes(path, CreateZipBytes(entries)); + return path; + } + + [TestMethod] + public void ConstructorRecoversInterruptedActivationBackupWhenTargetIsMissing() + { + string backup = Path.Combine(componentRoot, ".activate-backup-2.1.18-0123456789abcdef0123456789abcdef"); + Directory.CreateDirectory(backup); + File.WriteAllText(Path.Combine(backup, "dnscrypt-proxy.exe"), "old-binary"); + + using var manager = new DnsCryptComponentManager( + CreateHttpClient(new Dictionary>()), + componentRoot, + updateRoot, + [DnsCryptComponentManager.ReleaseSigningPublicKey]); + + string restored = Path.Combine(componentRoot, "2.1.18", "dnscrypt-proxy.exe"); + Assert.IsTrue(File.Exists(restored)); + Assert.AreEqual("old-binary", File.ReadAllText(restored)); + Assert.IsFalse(Directory.Exists(backup)); + } + + [TestMethod] + public void ConstructorCleansStalePreparedAndDownloadDirectories() + { + Directory.CreateDirectory(Path.Combine(componentRoot, ".prepared-2.1.18-stale")); + Directory.CreateDirectory(Path.Combine(componentRoot, ".install-stale")); + Directory.CreateDirectory(Path.Combine(componentRoot, "2.1.17")); + Directory.CreateDirectory(Path.Combine(updateRoot, "stale-operation")); + File.WriteAllText(Path.Combine(updateRoot, "stale.tmp"), "stale"); + + using var manager = new DnsCryptComponentManager( + CreateHttpClient(new Dictionary>()), + componentRoot, + updateRoot, + [DnsCryptComponentManager.ReleaseSigningPublicKey]); + + Assert.IsFalse(Directory.Exists(Path.Combine(componentRoot, ".prepared-2.1.18-stale"))); + Assert.IsFalse(Directory.Exists(Path.Combine(componentRoot, ".install-stale"))); + Assert.IsTrue(Directory.Exists(Path.Combine(componentRoot, "2.1.17"))); + Assert.IsFalse(Directory.EnumerateFileSystemEntries(updateRoot).Any()); + } + + private static byte[] CreateZipBytes(params (string Name, byte[] Content)[] entries) + { + using var stream = new MemoryStream(); + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach ((string entryName, byte[] content) in entries) + { + ZipArchiveEntry entry = archive.CreateEntry(entryName, CompressionLevel.SmallestSize); + using Stream output = entry.Open(); + output.Write(content, 0, content.Length); + } + } + return stream.ToArray(); + } + + private static MinisignFixture CreateMinisignFixture( + byte[] payload, + int seedOffset = 0, + bool prehashed = true) + { + byte[] seed = Enumerable.Range(0, 32).Select(value => (byte)(value + seedOffset)).ToArray(); + byte[] keyId = Enumerable.Range(1, 8).Select(value => (byte)(value + seedOffset)).ToArray(); + var privateKey = new Ed25519PrivateKeyParameters(seed); + byte[] publicKeyBytes = privateKey.GeneratePublicKey().GetEncoded(); + + byte[] signedPayload; + string algorithm; + string trustedComment; + if (prehashed) + { + var digest = new Blake2bDigest(512); + digest.BlockUpdate(payload, 0, payload.Length); + signedPayload = new byte[64]; + digest.DoFinal(signedPayload, 0); + algorithm = "ED"; + trustedComment = "timestamp:1700000000\tfile:dnscrypt-proxy.zip\thashed"; + } + else + { + signedPayload = payload; + algorithm = "Ed"; + trustedComment = "timestamp:1700000000\tfile:public-resolvers.md"; + } + + byte[] signature = Sign(privateKey, signedPayload); + byte[] trustedCommentBytes = Encoding.UTF8.GetBytes(trustedComment); + byte[] globalMessage = new byte[signature.Length + trustedCommentBytes.Length]; + Buffer.BlockCopy(signature, 0, globalMessage, 0, signature.Length); + Buffer.BlockCopy(trustedCommentBytes, 0, globalMessage, signature.Length, trustedCommentBytes.Length); + byte[] globalSignature = Sign(privateKey, globalMessage); + + byte[] publicKeyRecord = Concat(Encoding.ASCII.GetBytes("Ed"), keyId, publicKeyBytes); + byte[] signatureRecord = Concat(Encoding.ASCII.GetBytes(algorithm), keyId, signature); + string signatureText = string.Join("\n", new[] + { + "untrusted comment: signature from test key", + Convert.ToBase64String(signatureRecord), + "trusted comment: " + trustedComment, + Convert.ToBase64String(globalSignature), + string.Empty, + }); + + return new MinisignFixture(Convert.ToBase64String(publicKeyRecord), signatureText); + } + + private static byte[] Sign(Ed25519PrivateKeyParameters privateKey, byte[] message) + { + var signer = new Ed25519Signer(); + signer.Init(true, privateKey); + signer.BlockUpdate(message, 0, message.Length); + return signer.GenerateSignature(); + } + + private static byte[] Concat(params byte[][] arrays) + { + byte[] result = new byte[arrays.Sum(array => array.Length)]; + int offset = 0; + foreach (byte[] array in arrays) + { + Buffer.BlockCopy(array, 0, result, offset, array.Length); + offset += array.Length; + } + return result; + } + + private static string CreateReleaseJson( + string tag, + string archiveName, + string archiveUrl, + int archiveSize, + string digest, + string signatureName, + string signatureUrl) + { + string digestProperty = digest is null ? string.Empty : $",\"digest\":\"sha256:{digest}\""; + return "{" + + $"\"tag_name\":\"{tag}\",\"draft\":false,\"prerelease\":false,\"assets\":[" + + $"{{\"name\":\"{archiveName}\",\"browser_download_url\":\"{archiveUrl}\",\"size\":{archiveSize}{digestProperty}}}," + + $"{{\"name\":\"{signatureName}\",\"browser_download_url\":\"{signatureUrl}\",\"size\":1024}}" + + "]}"; + } + + private static HttpClient CreateHttpClient(Dictionary> responses) => + new(new FakeHttpMessageHandler(responses)); + + private static HttpResponseMessage JsonResponse(string json) => new(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }; + + private static HttpResponseMessage BytesResponse(byte[] bytes) => new(HttpStatusCode.OK) + { + Content = new ByteArrayContent(bytes), + }; + + private sealed record MinisignFixture(string PublicKey, string SignatureText); + + private sealed class FakeHttpMessageHandler : HttpMessageHandler + { + private readonly IReadOnlyDictionary> responses; + + public FakeHttpMessageHandler(IReadOnlyDictionary> responses) + { + this.responses = responses; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + string url = request.RequestUri?.AbsoluteUri ?? string.Empty; + if (!responses.TryGetValue(url, out Func responseFactory)) + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + return Task.FromResult(responseFactory()); + } + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptCoordinatorTests.cs b/Shadowsocks.UnitTests/DnsCryptCoordinatorTests.cs new file mode 100644 index 000000000..b2d192e98 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptCoordinatorTests.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptCoordinatorTests + { + [TestMethod] + public async Task ManagementTransactionsAreSerialized() + { + var coordinator = new DnsCryptCoordinator(); + var order = new List(); + var firstEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task first = coordinator.ExecuteExclusiveAsync( + "first", + async _ => + { + order.Add("first-enter"); + firstEntered.SetResult(true); + await releaseFirst.Task; + order.Add("first-exit"); + }); + + await firstEntered.Task; + Task second = coordinator.ExecuteExclusiveAsync( + "second", + _ => + { + order.Add("second-enter"); + return Task.CompletedTask; + }); + + await Task.Delay(25); + CollectionAssert.AreEqual(new[] { "first-enter" }, order); + Assert.IsTrue(coordinator.IsBusy); + Assert.AreEqual("first", coordinator.CurrentOperation); + + releaseFirst.SetResult(true); + await Task.WhenAll(first, second); + + CollectionAssert.AreEqual( + new[] { "first-enter", "first-exit", "second-enter" }, + order); + Assert.IsFalse(coordinator.IsBusy); + Assert.IsNull(coordinator.CurrentOperation); + } + + [TestMethod] + public async Task WaitingOperationCanBeCancelledWithoutBreakingGate() + { + var coordinator = new DnsCryptCoordinator(); + var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task holder = coordinator.ExecuteExclusiveAsync( + "holder", + async _ => + { + entered.SetResult(true); + await release.Task; + }); + await entered.Task; + + using var cancellation = new CancellationTokenSource(); + Task waiting = coordinator.ExecuteExclusiveAsync( + "waiting", + _ => Task.CompletedTask, + cancellation.Token); + cancellation.Cancel(); + + await Assert.ThrowsAsync(async () => await waiting); + release.SetResult(true); + await holder; + + bool ran = false; + await coordinator.ExecuteExclusiveAsync( + "after", + _ => + { + ran = true; + return Task.CompletedTask; + }); + Assert.IsTrue(ran); + } + + [TestMethod] + public async Task LifecycleCancellationCancelsActiveTransactionAndGateRemainsReusable() + { + var coordinator = new DnsCryptCoordinator(); + var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task active = coordinator.ExecuteExclusiveAsync( + "download", + async token => + { + entered.SetResult(true); + await Task.Delay(Timeout.InfiniteTimeSpan, token); + }); + await entered.Task; + + coordinator.CancelCurrentOperation(); + await Assert.ThrowsAsync(async () => await active); + + bool ran = false; + await coordinator.ExecuteExclusiveAsync( + "shutdown", + _ => + { + ran = true; + return Task.CompletedTask; + }); + Assert.IsTrue(ran); + Assert.IsFalse(coordinator.IsBusy); + } + + [TestMethod] + public void DnsCryptRuntimeRunsWheneverDnsCryptPolicyIsSelected() + { + Assert.IsTrue(Shadowsocks.Controller.ShadowsocksController.ShouldRunDnsCryptRuntime( + TrafficCaptureMode.User, DnsPolicyMode.DnsCrypt)); + Assert.IsTrue(Shadowsocks.Controller.ShadowsocksController.ShouldRunDnsCryptRuntime( + TrafficCaptureMode.Admin, DnsPolicyMode.DnsCrypt)); + Assert.IsFalse(Shadowsocks.Controller.ShadowsocksController.ShouldRunDnsCryptRuntime( + TrafficCaptureMode.Admin, DnsPolicyMode.System)); + } + + [TestMethod] + public async Task SuspendCancelsActiveAndQueuedTransactionsUntilResume() + { + var coordinator = new DnsCryptCoordinator(); + var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task active = coordinator.ExecuteExclusiveAsync( + "active", + async token => + { + entered.SetResult(true); + await Task.Delay(Timeout.InfiniteTimeSpan, token); + }); + await entered.Task; + + Task queued = coordinator.ExecuteExclusiveAsync( + "queued", + _ => Task.CompletedTask); + bool cleanupRan = false; + Task suspend = coordinator.SuspendAndExecuteAsync( + "shutdown", + _ => + { + cleanupRan = true; + return Task.CompletedTask; + }); + + await Assert.ThrowsAsync(async () => await active); + await Assert.ThrowsAsync(async () => await queued); + await suspend; + Assert.IsTrue(cleanupRan); + Assert.IsTrue(coordinator.IsSuspended); + + await Assert.ThrowsExactlyAsync(async () => + await coordinator.ExecuteExclusiveAsync("blocked", _ => Task.CompletedTask)); + + coordinator.Resume(); + bool resumed = false; + await coordinator.ExecuteExclusiveAsync( + "resumed", + _ => + { + resumed = true; + return Task.CompletedTask; + }); + Assert.IsTrue(resumed); + Assert.IsFalse(coordinator.IsSuspended); + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptCountrySelectorTests.cs b/Shadowsocks.UnitTests/DnsCryptCountrySelectorTests.cs new file mode 100644 index 000000000..07b6d4653 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptCountrySelectorTests.cs @@ -0,0 +1,243 @@ +#nullable enable annotations +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptCountrySelectorTests + { + [TestMethod] + public async Task AutomaticSelectionKeepsResolverInServerCountry() + { + var resolvers = new List + { + new("de-one", "DNSCrypt", false, true, true, true, "Hosted in Germany.", new[] { "203.0.113.10" }), + new("us-one", "DNSCrypt", false, true, true, true, "Hosted in the United States.", new[] { "198.51.100.10" }), + }; + var countries = new List { new("DE", "Germany", "🇩🇪", "192.0.2.1") }; + var config = new DnsCryptConfig(); + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, countries, config, + (host, _) => Task.FromResult(host.StartsWith("203.", StringComparison.Ordinal) + ? new IpCountryInfo("DE", "Germany", "🇩🇪", host) + : new IpCountryInfo("US", "United States", "🇺🇸", host)), + CancellationToken.None); + + Assert.IsTrue(selected.Contains("de-one", StringComparer.OrdinalIgnoreCase)); + Assert.IsFalse(selected.Contains("us-one", StringComparer.OrdinalIgnoreCase)); + } + + + [TestMethod] + public async Task AutomaticSelectionCoversAllServerCountries() + { + var resolvers = new List + { + new("de-one", "DNSCrypt", false, true, true, true, "Hosted in Germany.", new[] { "203.0.113.10:443" }), + new("us-one", "DNSCrypt", false, true, true, true, "Hosted in the United States.", new[] { "198.51.100.10:443" }), + new("fr-one", "DNSCrypt", false, true, true, true, "Hosted in France.", new[] { "192.0.2.20:443" }), + }; + var countries = new List + { + new("DE", "Germany", "🇩🇪", "192.0.2.1"), + new("US", "United States", "🇺🇸", "192.0.2.2"), + }; + var config = new DnsCryptConfig(); + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, countries, config, + (host, _) => Task.FromResult(host.StartsWith("203.", StringComparison.Ordinal) + ? new IpCountryInfo("DE", "Germany", "🇩🇪", host) + : host.StartsWith("198.", StringComparison.Ordinal) + ? new IpCountryInfo("US", "United States", "🇺🇸", host) + : new IpCountryInfo("FR", "France", "🇫🇷", host)), + CancellationToken.None); + + Assert.IsTrue(selected.Contains("de-one", StringComparer.OrdinalIgnoreCase)); + Assert.IsTrue(selected.Contains("us-one", StringComparer.OrdinalIgnoreCase)); + Assert.IsFalse(selected.Contains("fr-one", StringComparer.OrdinalIgnoreCase)); + } + + [TestMethod] + public async Task CountryCodeDoesNotMatchCommonDescriptionWord() + { + var resolvers = new List + { + new("fr-one", "DNSCrypt", false, true, true, true, "Fast resolver in France.", Array.Empty()), + }; + var countries = new List { new("IN", "India", "🇮🇳", "192.0.2.3") }; + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, countries, new DnsCryptConfig(), + (_, _) => Task.FromResult(null), + CancellationToken.None); + + Assert.AreEqual(0, selected.Count); + } + + [TestMethod] + public async Task AutomaticSelectionUsesEnrichedCountryAndPrefersLowerLatency() + { + var resolvers = new List + { + new("de-slow", "DNSCrypt", false, true, true, true, "", Array.Empty(), "DE", "Germany", "🇩🇪", 90), + new("de-fast", "DNSCrypt", false, true, true, true, "", Array.Empty(), "DE", "Germany", "🇩🇪", 18), + new("us-fast", "DNSCrypt", false, true, true, true, "", Array.Empty(), "US", "United States", "🇺🇸", 5), + }; + var countries = new List { new("DE", "Germany", "🇩🇪", "192.0.2.1") }; + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, countries, new DnsCryptConfig(), + (_, _) => Task.FromResult(null), + CancellationToken.None); + + Assert.AreEqual(1, selected.Count); + Assert.AreEqual("de-fast", selected[0]); + Assert.IsFalse(selected.Contains("de-slow", StringComparer.OrdinalIgnoreCase)); + Assert.IsFalse(selected.Contains("us-fast", StringComparer.OrdinalIgnoreCase)); + } + + [TestMethod] + public async Task AutomaticSelectionNeverUsesDohResolvers() + { + var resolvers = new List + { + new("de-doh-fast", "DoH", false, true, true, true, "Hosted in Germany.", Array.Empty(), "DE", "Germany", "🇩🇪", 5), + new("de-dnscrypt", "DNSCrypt", false, true, true, true, "Hosted in Germany.", Array.Empty(), "DE", "Germany", "🇩🇪", 35), + }; + var countries = new List { new("DE", "Germany", "🇩🇪", "192.0.2.1") }; + var config = new DnsCryptConfig { routeThroughShadowsocks = true }; + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, countries, config, + (_, _) => Task.FromResult(null), + CancellationToken.None); + + Assert.AreEqual(1, selected.Count); + Assert.AreEqual("de-dnscrypt", selected[0]); + } + + + [TestMethod] + public async Task MoscowResolverCountryComesFromEndpointIpOnly() + { + var resolver = new DnsCryptResolverInfo( + "totally-unrelated-name", + "DNSCrypt", + false, + true, + true, + true, + "Description intentionally contains no location.", + new[] { "mow01.dnscry.pt", "93.183.106.222:443" }); + + IpCountryInfo? located = await DnsCryptCountrySelector.ResolveResolverCountryByEndpointAsync( + resolver, + (host, _) => Task.FromResult(host == "93.183.106.222" + ? new IpCountryInfo("RU", "Russia", "🇷🇺", host) + : new IpCountryInfo("US", "United States", "🇺🇸", host)), + CancellationToken.None); + + Assert.IsNotNull(located); + Assert.AreEqual("RU", located.CountryCode); + Assert.AreEqual("Russia", located.CountryName); + Assert.AreEqual("93.183.106.222", located.Address); + } + + [TestMethod] + public async Task ResolverNameAndDescriptionNeverDefineCountry() + { + var resolvers = new List + { + new("dnscry.pt-moscow-ipv4", "DNSCrypt", false, true, true, true, + "Moscow Russia resolver.", new[] { "198.51.100.77" }), + }; + var countries = new List + { + new("RU", "Russia", "🇷🇺", "192.0.2.1"), + }; + + IReadOnlyList selected = await DnsCryptCountrySelector.SelectAsync( + resolvers, + countries, + new DnsCryptConfig(), + (host, _) => Task.FromResult( + new IpCountryInfo("US", "United States", "🇺🇸", host)), + CancellationToken.None); + + Assert.AreEqual(0, selected.Count); + } + + + [TestMethod] + public async Task ResolverWithEndpointsInDifferentCountriesHasNoSingleCountry() + { + var resolver = new DnsCryptResolverInfo( + "multi-endpoint", + "DoH", + false, + true, + true, + true, + string.Empty, + new[] { "203.0.113.10", "198.51.100.10" }); + + IpCountryInfo? located = await DnsCryptCountrySelector.ResolveResolverCountryByEndpointAsync( + resolver, + (host, _) => Task.FromResult(host.StartsWith("203.", StringComparison.Ordinal) + ? new IpCountryInfo("DE", "Germany", "🇩🇪", host) + : new IpCountryInfo("US", "United States", "🇺🇸", host)), + CancellationToken.None); + + Assert.IsNull(located); + } + + [TestMethod] + public void AutomaticFallbackAppliesResolverPrivacyFilters() + { + var resolvers = new List + { + new("fast-logs", "DNSCrypt", false, true, false, true, "", Array.Empty(), LatencyMs: 1), + new("filtered", "DNSCrypt", false, true, true, false, "", Array.Empty(), LatencyMs: 2), + new("eligible", "DoH", false, true, true, true, "", Array.Empty(), LatencyMs: 20), + }; + var config = new DnsCryptConfig + { + requireDnssec = true, + requireNoLog = true, + requireNoFilter = true, + ipv4Servers = true, + ipv6Servers = false, + }; + + IReadOnlyList selected = DnsCryptCountrySelector.SelectFallback(resolvers, config); + + Assert.AreEqual(1, selected.Count); + Assert.AreEqual("eligible", selected[0]); + } + + [TestMethod] + public void ResolverEndpointNormalizationRemovesPortAndIpv6Brackets() + { + Assert.AreEqual("1.2.3.4", IpCountryService.NormalizeHost("1.2.3.4:443")); + Assert.AreEqual("2001:db8::1", IpCountryService.NormalizeHost("[2001:db8::1]:443")); + Assert.AreEqual("resolver.example", IpCountryService.NormalizeHost("resolver.example:443")); + } + + [TestMethod] + public void CountryCodeProducesFlagEmoji() + { + Assert.AreEqual("🇩🇪", IpCountryService.CountryCodeToFlag("DE")); + Assert.AreEqual("🇺🇸", IpCountryService.CountryCodeToFlag("us")); + Assert.AreEqual(string.Empty, IpCountryService.CountryCodeToFlag("USA")); + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptMaintenancePolicyTests.cs b/Shadowsocks.UnitTests/DnsCryptMaintenancePolicyTests.cs new file mode 100644 index 000000000..67eb068d0 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptMaintenancePolicyTests.cs @@ -0,0 +1,85 @@ +#nullable enable +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptMaintenancePolicyTests + { + private static readonly DateTimeOffset Now = new(2026, 8, 18, 12, 0, 0, TimeSpan.Zero); + + [TestMethod] + public void AutomaticCheckRequiresInstalledComponentAndEnabledSetting() + { + DnsCryptConfig enabled = new() { autoUpdate = true }; + DnsCryptConfig disabled = new() { autoUpdate = false }; + DnsCryptComponentStatus installed = Status(lastCheck: null); + DnsCryptComponentStatus missing = new(false, null, null, null, true, null); + + Assert.IsTrue(DnsCryptMaintenancePolicy.IsAutomaticCheckDue(installed, enabled, Now)); + Assert.IsFalse(DnsCryptMaintenancePolicy.IsAutomaticCheckDue(installed, disabled, Now)); + Assert.IsFalse(DnsCryptMaintenancePolicy.IsAutomaticCheckDue(missing, enabled, Now)); + } + + [TestMethod] + public void AutomaticCheckIsNeverMoreFrequentThanTwentyFourHours() + { + DnsCryptConfig config = new() { autoUpdate = true }; + Assert.IsFalse(DnsCryptMaintenancePolicy.IsAutomaticCheckDue( + Status(Now - TimeSpan.FromHours(23) - TimeSpan.FromMinutes(59)), config, Now)); + Assert.IsTrue(DnsCryptMaintenancePolicy.IsAutomaticCheckDue( + Status(Now - TimeSpan.FromHours(24)), config, Now)); + } + + [TestMethod] + public void SmallFutureClockSkewDoesNotTriggerImmediateAutomaticCheck() + { + DnsCryptConfig config = new() { autoUpdate = true }; + DnsCryptComponentStatus status = Status(Now + TimeSpan.FromMinutes(5)); + + Assert.IsFalse(DnsCryptMaintenancePolicy.IsAutomaticCheckDue(status, config, Now)); + Assert.AreEqual( + DnsCryptMaintenancePolicy.IdleRecheckInterval, + DnsCryptMaintenancePolicy.GetDelayUntilNextAutomaticCheck(status, config, Now)); + } + + [TestMethod] + public void LargeFutureClockSkewIsRepairedByRunningOneCheckNow() + { + DnsCryptConfig config = new() { autoUpdate = true }; + DnsCryptComponentStatus status = Status(Now + TimeSpan.FromDays(365)); + + Assert.IsTrue(DnsCryptMaintenancePolicy.IsAutomaticCheckDue(status, config, Now)); + Assert.AreEqual( + TimeSpan.Zero, + DnsCryptMaintenancePolicy.GetDelayUntilNextAutomaticCheck(status, config, Now)); + } + + [TestMethod] + public void DueCheckReturnsZeroDelayAndDisabledModeOnlyPollsLocalState() + { + Assert.AreEqual( + TimeSpan.Zero, + DnsCryptMaintenancePolicy.GetDelayUntilNextAutomaticCheck( + Status(null), new DnsCryptConfig { autoUpdate = true }, Now)); + Assert.AreEqual( + DnsCryptMaintenancePolicy.IdleRecheckInterval, + DnsCryptMaintenancePolicy.GetDelayUntilNextAutomaticCheck( + Status(null), new DnsCryptConfig { autoUpdate = false }, Now)); + } + + private static DnsCryptComponentStatus Status(DateTimeOffset? lastCheck) + { + return new DnsCryptComponentStatus( + true, + new Version(2, 1, 18), + null, + @"C:\DNSCrypt\dnscrypt-proxy.exe", + true, + lastCheck); + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptResolverCatalogBootstrapperTests.cs b/Shadowsocks.UnitTests/DnsCryptResolverCatalogBootstrapperTests.cs new file mode 100644 index 000000000..6d13acd12 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptResolverCatalogBootstrapperTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptResolverCatalogBootstrapperTests + { + private string root; + + [TestInitialize] + public void Initialize() + { + root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", "DnsCryptCatalog", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + } + + [TestCleanup] + public void Cleanup() + { + try { Directory.Delete(root, recursive: true); } catch { } + } + + [TestMethod] + public async Task DohResolverFallsBackFromCloudflareToGoogle() + { + var providers = new List(); + using var resolver = new ShadowsocksDohResolver( + "127.0.0.1", + 1080, + (provider, query, _) => + { + providers.Add(provider.Name); + if (string.Equals(provider.Name, "Cloudflare", StringComparison.Ordinal)) + throw new HttpRequestException("Synthetic Cloudflare failure."); + return Task.FromResult(BuildAddressResponse(query, IPAddress.Parse("203.0.113.7"))); + }); + + IPAddress[] addresses = await resolver.ResolveAsync("example.test", CancellationToken.None); + + CollectionAssert.AreEqual(new[] { "Cloudflare", "Google" }, providers.ToArray()); + CollectionAssert.AreEqual(new[] { IPAddress.Parse("203.0.113.7") }, addresses); + } + + [TestMethod] + public void DohWireParserAcceptsIpv4AndRejectsMismatchedTransaction() + { + const ushort transactionId = 0x1234; + byte[] query = ShadowsocksDohResolver.BuildQuery("example.test", 1, transactionId); + byte[] response = BuildAddressResponse(query, IPAddress.Parse("203.0.113.7")); + + CollectionAssert.AreEqual( + new[] { IPAddress.Parse("203.0.113.7") }, + ShadowsocksDohResolver.ParseAddresses(response, transactionId, 1)); + + Assert.ThrowsExactly(() => + ShadowsocksDohResolver.ParseAddresses(response, 0x4321, 1)); + } + + [TestMethod] + public async Task CatalogBootstrapDownloadsCatalogAndSignatureBeforePublishing() + { + byte[] catalog = Encoding.UTF8.GetBytes("## resolver\n\nsdns://signed\n"); + byte[] signature = Encoding.UTF8.GetBytes("synthetic minisign"); + var requests = new List(); + var bootstrapper = new DnsCryptResolverCatalogBootstrapper( + (uri, _) => + { + requests.Add(uri.AbsoluteUri); + return Task.FromResult(uri.AbsolutePath.EndsWith(".minisig", StringComparison.OrdinalIgnoreCase) + ? signature + : catalog); + }, + (path, signatureText, key) => + File.ReadAllBytes(path).SequenceEqual(catalog) + && signatureText == Encoding.UTF8.GetString(signature) + && key == DnsCryptTomlGenerator.PublicResolversMinisignKey); + + await bootstrapper.EnsureFreshAsync(root, "127.0.0.1", 1080, CancellationToken.None); + + CollectionAssert.AreEqual(catalog, File.ReadAllBytes(Path.Combine(root, "public-resolvers.md"))); + CollectionAssert.AreEqual(signature, File.ReadAllBytes(Path.Combine(root, "public-resolvers.md.minisig"))); + Assert.IsTrue(requests.Any(url => url.EndsWith("public-resolvers.md", StringComparison.Ordinal))); + Assert.IsTrue(requests.Any(url => url.EndsWith("public-resolvers.md.minisig", StringComparison.Ordinal))); + } + + [TestMethod] + public async Task CatalogBootstrapRejectsInvalidSignatureWithoutPublishing() + { + byte[] catalog = Encoding.UTF8.GetBytes("untrusted catalog"); + byte[] signature = Encoding.UTF8.GetBytes("untrusted signature"); + var bootstrapper = new DnsCryptResolverCatalogBootstrapper( + (uri, _) => Task.FromResult(uri.AbsolutePath.EndsWith(".minisig", StringComparison.OrdinalIgnoreCase) + ? signature + : catalog), + (_, _, _) => false); + + await Assert.ThrowsExactlyAsync(() => + bootstrapper.EnsureFreshAsync(root, "127.0.0.1", 1080, CancellationToken.None)); + + Assert.IsFalse(File.Exists(Path.Combine(root, "public-resolvers.md"))); + Assert.IsFalse(File.Exists(Path.Combine(root, "public-resolvers.md.minisig"))); + } + + [TestMethod] + public async Task FreshVerifiedCatalogAvoidsNetwork() + { + string catalogPath = Path.Combine(root, "public-resolvers.md"); + string signaturePath = Path.Combine(root, "public-resolvers.md.minisig"); + File.WriteAllText(catalogPath, "cached catalog"); + File.WriteAllText(signaturePath, "cached signature"); + var bootstrapper = new DnsCryptResolverCatalogBootstrapper( + (_, _) => throw new AssertFailedException("Fresh verified cache must not use the network."), + (_, _, _) => true); + + await bootstrapper.EnsureFreshAsync(root, "127.0.0.1", 1080, CancellationToken.None); + } + + [TestMethod] + public async Task VerifiedStaleCatalogIsFallbackWhenDohRefreshFails() + { + string catalogPath = Path.Combine(root, "public-resolvers.md"); + string signaturePath = Path.Combine(root, "public-resolvers.md.minisig"); + File.WriteAllText(catalogPath, "cached catalog"); + File.WriteAllText(signaturePath, "cached signature"); + DateTime stale = DateTime.UtcNow.AddDays(-2); + File.SetLastWriteTimeUtc(catalogPath, stale); + File.SetLastWriteTimeUtc(signaturePath, stale); + var bootstrapper = new DnsCryptResolverCatalogBootstrapper( + (_, _) => throw new HttpRequestException("Synthetic DoH/source failure."), + (_, _, _) => true); + + await bootstrapper.EnsureFreshAsync(root, "127.0.0.1", 1080, CancellationToken.None); + + Assert.AreEqual("cached catalog", File.ReadAllText(catalogPath)); + Assert.AreEqual("cached signature", File.ReadAllText(signaturePath)); + } + + private static byte[] BuildAddressResponse(byte[] query, IPAddress address) + { + int questionLength = query.Length - 12; + byte[] addressBytes = address.GetAddressBytes(); + ushort type = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? (ushort)1 : (ushort)28; + byte[] response = new byte[12 + questionLength + 12 + addressBytes.Length]; + query.AsSpan(0, 2).CopyTo(response); + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(2, 2), 0x8180); + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(4, 2), 1); + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(6, 2), 1); + query.AsSpan(12).CopyTo(response.AsSpan(12)); + int offset = 12 + questionLength; + response[offset++] = 0xC0; + response[offset++] = 0x0C; + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(offset, 2), type); + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(offset + 2, 2), 1); + BinaryPrimitives.WriteUInt32BigEndian(response.AsSpan(offset + 4, 4), 60); + BinaryPrimitives.WriteUInt16BigEndian(response.AsSpan(offset + 8, 2), checked((ushort)addressBytes.Length)); + offset += 10; + addressBytes.CopyTo(response, offset); + return response; + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptRuntimeManagerTests.cs b/Shadowsocks.UnitTests/DnsCryptRuntimeManagerTests.cs new file mode 100644 index 000000000..079ab4fa6 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptRuntimeManagerTests.cs @@ -0,0 +1,776 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + [DoNotParallelize] + public class DnsCryptRuntimeManagerTests + { + private string root; + private string runtimeRoot; + private string executablePath; + + [TestInitialize] + public void Initialize() + { + root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", "DnsCryptRuntime", Guid.NewGuid().ToString("N")); + runtimeRoot = Path.Combine(root, "Runtime", "DNSCryptProxy"); + executablePath = Path.Combine(root, "Components", "DNSCryptProxy", "2.1.18", "dnscrypt-proxy.exe"); + Directory.CreateDirectory(Path.GetDirectoryName(executablePath)); + File.WriteAllBytes(executablePath, new byte[] { 0x4D, 0x5A }); + } + + [TestCleanup] + public void Cleanup() + { + try { Directory.Delete(root, recursive: true); } catch { } + } + + [TestMethod] + public async Task StartValidatesVersionStartsAndHealthChecksWithoutBlockingPreflight() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + + DnsCryptRuntimeStatus status = await manager.StartAsync(CreateConcreteRuntimeOptions()); + + Assert.AreEqual(DnsCryptRuntimeState.Running, status.State); + Assert.AreEqual(new Version(2, 1, 18), status.Version); + Assert.AreEqual(38471, status.Port); + Assert.AreEqual(7000, status.ProcessId); + Assert.IsTrue(File.Exists(status.ConfigPath)); + Assert.AreEqual(1, platform.Commands.Count); + CollectionAssert.AreEqual(new[] { "-version" }, platform.Commands[0].ToArray()); + Assert.IsFalse(platform.Commands.Any(command => command.Contains("-check")), + "Normal runtime startup must not block on dnscrypt-proxy -check network/source initialization."); + CollectionAssert.AreEqual(new[] { "-config", status.ConfigPath }, platform.StartArguments.Single().ToArray()); + + await manager.StopAsync(); + Assert.AreEqual(DnsCryptRuntimeState.Stopped, manager.GetStatus().State); + Assert.IsTrue(platform.Processes[0].WasKilled); + } + + [TestMethod] + public async Task StartRejectsMissingComponent() + { + var platform = new FakeRuntimePlatform(); + using var manager = new DnsCryptRuntimeManager( + () => new DnsCryptComponentStatus(false, null, null, null, true, null), + runtimeRoot, + platform, + () => 38471, + (_, _, _) => Task.FromResult(true), + [TimeSpan.Zero], + startupTimeout: TimeSpan.FromMilliseconds(40)); + + await Assert.ThrowsExactlyAsync(() => + manager.StartAsync(CreateConcreteRuntimeOptions())); + Assert.AreEqual(DnsCryptRuntimeState.NotInstalled, manager.GetStatus().State); + Assert.AreEqual(0, platform.Commands.Count); + } + + [TestMethod] + public async Task StartRejectsExecutableVersionMismatch() + { + var platform = new FakeRuntimePlatform { VersionOutput = "2.1.17" }; + using var manager = CreateManager(platform, health: true, port: 38471); + + await Assert.ThrowsExactlyAsync(() => + manager.StartAsync(CreateConcreteRuntimeOptions())); + + Assert.AreEqual(DnsCryptRuntimeState.Failed, manager.GetStatus().State); + Assert.AreEqual(0, platform.Processes.Count); + } + + [TestMethod] + public async Task PreparedAutomaticValidationUsesSignedCatalogBeforeRuntimeHealthCheck() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptPreparedComponent prepared = CreatePreparedComponent(); + + await manager.ValidatePreparedAsync( + prepared, + new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + int versionCommandIndex = platform.Commands.FindIndex(command => command.Contains("-version")); + int listCommandIndex = platform.Commands.FindIndex(command => command.Contains("-list-all") && command.Contains("-json")); + Assert.IsTrue(versionCommandIndex >= 0); + Assert.IsTrue(listCommandIndex > versionCommandIndex, "Prepared binary version must be verified before signed-catalog discovery."); + Assert.IsTrue(platform.StartedConfigText.Contains("server_names = ['test-resolver']", StringComparison.Ordinal)); + Assert.IsTrue(platform.StartedConfigText.Contains("[static.'test-resolver']", StringComparison.Ordinal)); + Assert.IsTrue(platform.StartedConfigText.Contains("stamp = 'sdns://test'", StringComparison.Ordinal)); + Assert.IsFalse(platform.StartedConfigText.Contains("[sources.public-resolvers]", StringComparison.Ordinal)); + } + + [TestMethod] + public async Task PreparedValidationRejectsVersionMismatchBeforeCatalogDiscovery() + { + var platform = new FakeRuntimePlatform { VersionOutput = "2.1.17" }; + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptPreparedComponent prepared = CreatePreparedComponent(); + + await Assert.ThrowsExactlyAsync(() => + manager.ValidatePreparedAsync(prepared, new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig()))); + + Assert.IsTrue(platform.Commands.Any(command => command.Contains("-version"))); + Assert.IsFalse(platform.Commands.Any(command => command.Contains("-list-all")), + "A mismatched prepared binary must fail before it is allowed to perform catalog/network operations."); + Assert.AreEqual(0, platform.Processes.Count); + } + + [TestMethod] + public async Task PreparedValidationRejectsInvalidGeneratedConfiguration() + { + var platform = new FakeRuntimePlatform { CheckExitCode = 2, CheckError = "bad config" }; + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptPreparedComponent prepared = CreatePreparedComponent(); + + await Assert.ThrowsExactlyAsync(() => + manager.ValidatePreparedAsync(prepared, new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig()))); + + Assert.AreEqual(0, platform.Processes.Count); + } + + [TestMethod] + public async Task PreparedValidationCheckTimeoutFallsBackToRuntimeHealthCheck() + { + var platform = new FakeRuntimePlatform { CheckThrowsTimeout = true }; + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptPreparedComponent prepared = CreatePreparedComponent(); + + await manager.ValidatePreparedAsync(prepared, new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + Assert.IsTrue(platform.Commands.Any(command => command.Contains("-check"))); + Assert.AreEqual(1, platform.Processes.Count); + Assert.IsTrue(platform.Processes[0].WasKilled, "Validation candidate must be stopped after a successful DNS health probe."); + } + + [TestMethod] + public async Task FailedHealthCheckTerminatesStartedProcess() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: false, port: 38471); + + await Assert.ThrowsExactlyAsync(() => + manager.StartAsync(CreateConcreteRuntimeOptions())); + + Assert.AreEqual(DnsCryptRuntimeState.Failed, manager.GetStatus().State); + Assert.AreEqual(1, platform.Processes.Count); + Assert.IsTrue(platform.Processes[0].WasKilled); + } + + [TestMethod] + public async Task UnexpectedExitRestartsRuntimeWithSavedConfig() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471, restartDelays: [TimeSpan.Zero]); + await manager.StartAsync(CreateConcreteRuntimeOptions()); + + platform.Processes[0].TriggerUnexpectedExit(); + await WaitUntilAsync(() => platform.Processes.Count >= 2 && manager.GetStatus().State == DnsCryptRuntimeState.Running); + + Assert.AreEqual(2, platform.Processes.Count); + Assert.AreEqual(7001, manager.GetStatus().ProcessId); + } + + [TestMethod] + public async Task ExitDuringRunningPromotionNeverLeavesStaleRunningStatus() + { + var platform = new FakeRuntimePlatform + { + ExitFirstProcessOnExitedSubscription = true, + }; + using var manager = CreateManager( + platform, + health: true, + port: 38471, + restartDelays: [TimeSpan.FromSeconds(1)]); + + DnsCryptRuntimeStatus initial = await manager.StartAsync( + CreateConcreteRuntimeOptions()); + Assert.AreEqual(7000, initial.ProcessId); + + await WaitUntilAsync(() => manager.GetStatus().State == DnsCryptRuntimeState.Failed); + DnsCryptRuntimeStatus status = manager.GetStatus(); + Assert.AreEqual(0, status.ProcessId); + } + + [TestMethod] + public async Task ExplicitStopDoesNotTriggerCrashRecovery() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471, restartDelays: [TimeSpan.Zero]); + await manager.StartAsync(CreateConcreteRuntimeOptions()); + + await manager.StopAsync(); + await Task.Delay(50); + + Assert.AreEqual(1, platform.Processes.Count); + Assert.AreEqual(DnsCryptRuntimeState.Stopped, manager.GetStatus().State); + } + + [TestMethod] + public async Task UnexpectedExitStopsAfterConfiguredRestartLimit() + { + var platform = new FakeRuntimePlatform(); + int healthCalls = 0; + using var manager = new DnsCryptRuntimeManager( + () => new DnsCryptComponentStatus(true, new Version(2, 1, 18), null, executablePath, true, null), + runtimeRoot, + platform, + () => 38471, + (_, _, _) => Task.FromResult(Interlocked.Increment(ref healthCalls) == 1), + [TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero], + startupTimeout: TimeSpan.FromMilliseconds(40)); + + await manager.StartAsync(CreateConcreteRuntimeOptions()); + platform.Processes[0].TriggerUnexpectedExit(); + + await WaitUntilAsync(() => platform.Processes.Count >= 4 && manager.GetStatus().State == DnsCryptRuntimeState.Failed); + + Assert.AreEqual(4, platform.Processes.Count); + StringAssert.Contains(manager.GetStatus().LastError, "3 automatic restart attempts"); + } + + [TestMethod] + public void DnsPacketValidationRequiresMatchingResponseId() + { + byte[] query = DnsCryptRuntimeManager.BuildDnsAQuery(0x1234, "example.com"); + Assert.IsTrue(query.Length > 20); + Assert.AreEqual(0x12, query[0]); + Assert.AreEqual(0x34, query[1]); + + byte[] response = query.ToArray(); + response[2] = 0x81; + response[3] = 0x80; + + Assert.IsTrue(DnsCryptRuntimeManager.IsValidDnsResponse(response, 0x1234)); + Assert.IsFalse(DnsCryptRuntimeManager.IsValidDnsResponse(response, 0x9999)); + Assert.IsFalse(DnsCryptRuntimeManager.IsValidDnsResponse(response.Take(12).ToArray(), 0x1234)); + } + + [TestMethod] + public void DnsPacketValidationRejectsNxDomainAndWrongQuestion() + { + byte[] query = DnsCryptRuntimeManager.BuildDnsAQuery(0x4321, "example.com"); + byte[] nxDomain = query.ToArray(); + nxDomain[2] = 0x81; + nxDomain[3] = 0x83; + Assert.IsFalse(DnsCryptRuntimeManager.IsValidDnsResponse(nxDomain, 0x4321)); + + byte[] wrongQuestion = DnsCryptRuntimeManager.BuildDnsAQuery(0x4321, "example.net"); + wrongQuestion[2] = 0x81; + wrongQuestion[3] = 0x80; + Assert.IsFalse(DnsCryptRuntimeManager.IsValidDnsResponse(wrongQuestion, 0x4321, "example.com")); + } + + [TestMethod] + public async Task ValidatePreparedRunsCandidateWithoutReplacingActiveRuntime() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptRuntimeStatus running = await manager.StartAsync(CreateConcreteRuntimeOptions()); + + DnsCryptPreparedComponent prepared = CreatePreparedComponent(); + + await manager.ValidatePreparedAsync(prepared, new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + DnsCryptRuntimeStatus after = manager.GetStatus(); + Assert.AreEqual(DnsCryptRuntimeState.Running, after.State); + Assert.AreEqual(running.ProcessId, after.ProcessId); + Assert.AreEqual(2, platform.Processes.Count); + Assert.IsTrue(platform.Processes[1].WasKilled, "Validation candidate must always be terminated."); + await manager.StopAsync(); + } + + [TestMethod] + public async Task UdpHealthCheckAcceptsMatchingDnsResponse() + { + using var server = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + int port = ((IPEndPoint)server.Client.LocalEndPoint).Port; + Task responder = Task.Run(async () => + { + UdpReceiveResult request = await server.ReceiveAsync(); + byte[] response = request.Buffer.ToArray(); + response[2] = 0x81; + response[3] = 0x80; + await server.SendAsync(response, response.Length, request.RemoteEndPoint); + }); + + bool healthy = await DnsCryptRuntimeManager.DnsHealthCheckAsync(port, TimeSpan.FromSeconds(1), CancellationToken.None); + await responder; + + Assert.IsTrue(healthy); + } + + [TestMethod] + public void AllocatedLoopbackPortIsInUsableRange() + { + int port = DnsCryptRuntimeManager.AllocateLoopbackPort(); + Assert.IsTrue(port is > 0 and <= 65535); + } + + [TestMethod] + public void VersionParserAcceptsPlainAndVPrefixedOutput() + { + Assert.AreEqual(new Version(2, 1, 18), DnsCryptRuntimeManager.ParseReportedVersion("2.1.18\r\n")); + Assert.AreEqual(new Version(2, 1, 18), DnsCryptRuntimeManager.ParseReportedVersion("v2.1.18\n")); + Assert.IsNull(DnsCryptRuntimeManager.ParseReportedVersion("not-a-version")); + } + + [TestMethod] + public void ResolverListParserReadsCurrentDnsCryptJsonShape() + { + const string json = + "[" + + "{\"name\":\"cloudflare\",\"proto\":\"DNSCrypt\",\"ipv6\":false,\"addrs\":[\"1.1.1.1\"],\"ports\":[443],\"dnssec\":true,\"nolog\":true,\"nofilter\":true,\"description\":\"Cloudflare\",\"stamp\":\"sdns://test\"}," + + "{\"name\":\"quad9-doh\",\"proto\":\"DoH\",\"ipv6\":true,\"addrs\":[\"[2620:fe::fe]\"],\"ports\":[443],\"dnssec\":true,\"nolog\":true,\"nofilter\":false,\"description\":\"Quad9\",\"stamp\":\"sdns://test2\"}" + + "]"; + + IReadOnlyList resolvers = DnsCryptRuntimeManager.ParseResolverList(json); + + Assert.AreEqual(2, resolvers.Count); + Assert.AreEqual("cloudflare", resolvers[0].Name); + Assert.AreEqual("DNSCrypt", resolvers[0].Protocol); + Assert.IsTrue(resolvers[0].Dnssec == true); + Assert.IsTrue(resolvers[0].NoLog); + Assert.AreEqual(443, resolvers[0].Ports[0]); + Assert.AreEqual("1.1.1.1", resolvers[0].Addresses[0]); + Assert.AreEqual("sdns://test", resolvers[0].Stamp); + Assert.AreEqual("quad9-doh", resolvers[1].Name); + Assert.IsTrue(resolvers[1].IPv6); + } + + [TestMethod] + public void ResolverLatencyProbeBuildsEndpointFromCatalogAddressAndPort() + { + var resolver = new DnsCryptResolverInfo( + "test", "DoH", false, true, true, true, "", new[] { "resolver.example", "1.1.1.1" }) + { + Ports = new[] { 8443 }, + }; + + Assert.IsTrue(DnsCryptResolverLatencyProbe.TryGetEndpoint(resolver, out IPEndPoint endpoint)); + Assert.AreEqual(IPAddress.Parse("1.1.1.1"), endpoint.Address); + Assert.AreEqual(8443, endpoint.Port); + } + + [TestMethod] + public void ResolverLatencyParserReadsRttAndKeepsFastestProbe() + { + const string output = "[2026-08-18 18:07:32] [NOTICE] [de-one] OK (DNSCrypt) - rtt: 41ms\n" + + "[2026-08-18 18:07:32] [NOTICE] [de-one] OK (DNSCrypt) - rtt: 35ms - additional certificate\n" + + "[2026-08-18 18:07:32] [NOTICE] [us-one] TIMEOUT"; + + IReadOnlyDictionary latencies = DnsCryptRuntimeManager.ParseResolverLatencies(output); + + Assert.AreEqual(35, latencies["de-one"]); + Assert.IsFalse(latencies.ContainsKey("us-one")); + } + + [TestMethod] + public void SelectedResolverParserReadsDnsCryptLowestLatencyLine() + { + Assert.AreEqual( + "cs-dc", + DnsCryptRuntimeManager.ParseSelectedResolverName( + "[2026-08-19 07:15:12] [NOTICE] Server with the lowest initial latency: cs-dc (rtt: 189ms), live servers: 1")); + Assert.AreEqual(string.Empty, DnsCryptRuntimeManager.ParseSelectedResolverName("Source [public-resolvers] loaded")); + } + + [TestMethod] + public async Task RuntimeOutputPopulatesActiveResolverAndActualLatency() + { + var platform = new FakeRuntimePlatform + { + RuntimeOutputLines = + [ + "[NOTICE] [cs-dc] OK (DNSCrypt) - rtt: 189ms", + "[NOTICE] Server with the lowest initial latency: cs-dc (rtt: 189ms), live servers: 1", + ], + }; + using var manager = CreateManager(platform, health: true, port: 38471); + + await manager.StartAsync(CreateConcreteRuntimeOptions()); + + CollectionAssert.AreEqual(new[] { "cs-dc" }, manager.GetActiveResolverNames().ToArray()); + Assert.AreEqual(189, manager.GetResolverLatencies()["cs-dc"]); + } + + [TestMethod] + public async Task ActiveRuntimeUsesPinnedStaticResolverWithoutRemoteSources() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + + await manager.StartAsync(CreateConcreteRuntimeOptions()); + + StringAssert.Contains(platform.StartedConfigText, "[static.'test-resolver']"); + StringAssert.Contains(platform.StartedConfigText, "stamp = 'sdns://test'"); + Assert.IsFalse(platform.StartedConfigText.Contains("[sources.public-resolvers]", StringComparison.Ordinal)); + StringAssert.Contains(platform.StartedConfigText, "bootstrap_resolvers = []"); + } + + [TestMethod] + public void ResolverProbeDiagnosticsAreVerboseOnly() + { + Assert.IsTrue(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [NOTICE] [de-one] OK (DNSCrypt) - rtt: 35ms")); + Assert.IsTrue(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [NOTICE] [de-one] using the post-quantum X-Wing key exchange")); + Assert.IsTrue(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [WARNING] [us-one] TIMEOUT")); + Assert.IsTrue(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [NOTICE] Server with the lowest initial latency: de-one (rtt: 35ms), live servers: 24")); + Assert.IsFalse(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [NOTICE] Now listening to 127.0.0.1:53000 [UDP]")); + Assert.IsFalse(DnsCryptRuntimeManager.IsResolverProbeDiagnostic( + "[2026-08-18 18:07:32] [NOTICE] Source [public-resolvers] loaded")); + } + + [TestMethod] + public async Task ListResolversUsesDnsCryptSignedCatalogCommand() + { + var platform = new FakeRuntimePlatform + { + ListOutput = "[{\"name\":\"cloudflare\",\"proto\":\"DNSCrypt\",\"ipv6\":false,\"dnssec\":true,\"nolog\":true,\"nofilter\":true,\"description\":\"Cloudflare\",\"stamp\":\"sdns://test\"}]", + }; + using var manager = CreateManager(platform, health: true, port: 38471); + + IReadOnlyList resolvers = await manager.ListResolversAsync( + new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + Assert.AreEqual(1, resolvers.Count); + Assert.AreEqual("cloudflare", resolvers[0].Name); + Assert.IsTrue(platform.Commands.Any(command => command.Contains("-list-all") && command.Contains("-json"))); + Assert.IsFalse(Directory.EnumerateDirectories(runtimeRoot, ".list-*", SearchOption.TopDirectoryOnly).Any()); + } + + [TestMethod] + public async Task ResolverCatalogBootstrapRunsBeforeDnscryptListCommand() + { + var platform = new FakeRuntimePlatform { ListOutput = "[]" }; + var bootstrapper = new FakeCatalogBootstrapper(); + using var manager = CreateManager(platform, health: true, port: 38471, bootstrapper: bootstrapper); + + await manager.ListResolversAsync(new DnsCryptRuntimeStartOptions(new DnsCryptConfig(), 1080) + { + ShadowsocksSocks5Host = "127.0.0.1", + }); + + Assert.AreEqual(1, bootstrapper.CallCount); + Assert.AreEqual("127.0.0.1", bootstrapper.LastHost); + Assert.AreEqual(1080, bootstrapper.LastPort); + Assert.IsTrue(platform.ListCommandHadResolverCache); + } + + [TestMethod] + public async Task ResolverCatalogRefreshPersistsSignedCacheForFutureMaintenance() + { + var platform = new FakeRuntimePlatform + { + ListOutput = "[]", + WriteResolverCacheOnList = true, + }; + using var manager = CreateManager(platform, health: true, port: 38471); + + await manager.ListResolversAsync(new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + string persisted = Path.Combine(runtimeRoot, "public-resolvers.md"); + string persistedSignature = Path.Combine(runtimeRoot, "public-resolvers.md.minisig"); + Assert.IsTrue(File.Exists(persisted)); + Assert.IsTrue(File.Exists(persistedSignature)); + Assert.AreEqual("signed resolver cache", File.ReadAllText(persisted)); + Assert.AreEqual("signed resolver signature", File.ReadAllText(persistedSignature)); + } + + [TestMethod] + public async Task ResolverCatalogListingReusesExistingSignedSourceCache() + { + Directory.CreateDirectory(runtimeRoot); + File.WriteAllText(Path.Combine(runtimeRoot, "public-resolvers.md"), "cached signed source"); + File.WriteAllText(Path.Combine(runtimeRoot, "public-resolvers.md.minisig"), "cached signed signature"); + var platform = new FakeRuntimePlatform + { + ListOutput = "[]", + }; + using var manager = CreateManager(platform, health: true, port: 38471); + + await manager.ListResolversAsync(new DnsCryptRuntimeStartOptions(CreateDirectDnsCryptConfig())); + + Assert.IsTrue(platform.ListCommandHadResolverCache); + } + + [TestMethod] + public async Task UpdatingStatePreservesRunningProcessAndRestoresRunning() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + DnsCryptRuntimeStatus running = await manager.StartAsync(CreateConcreteRuntimeOptions()); + + DnsCryptRuntimeStatus snapshot = manager.BeginUpdate(); + DnsCryptRuntimeStatus updating = manager.GetStatus(); + Assert.AreEqual(DnsCryptRuntimeState.Updating, updating.State); + Assert.AreEqual(running.ProcessId, updating.ProcessId); + Assert.AreEqual(running.Port, updating.Port); + Assert.IsTrue(updating.IsServing); + Assert.IsFalse(updating.IsRunning); + + manager.EndUpdate(snapshot, succeeded: false); + Assert.AreEqual(DnsCryptRuntimeState.Running, manager.GetStatus().State); + Assert.AreEqual(running.ProcessId, manager.GetStatus().ProcessId); + await manager.StopAsync(); + } + + [TestMethod] + public void UpdatingStoppedRuntimeCompletesAsStopped() + { + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + + DnsCryptRuntimeStatus snapshot = manager.BeginUpdate(); + Assert.AreEqual(DnsCryptRuntimeState.Updating, manager.GetStatus().State); + manager.EndUpdate(snapshot, succeeded: true); + Assert.AreEqual(DnsCryptRuntimeState.Stopped, manager.GetStatus().State); + } + + [TestMethod] + public void ConstructorCleansStaleRuntimeOperationDirectories() + { + Directory.CreateDirectory(Path.Combine(runtimeRoot, ".validate-stale")); + Directory.CreateDirectory(Path.Combine(runtimeRoot, ".list-stale")); + Directory.CreateDirectory(Path.Combine(runtimeRoot, "keep")); + + var platform = new FakeRuntimePlatform(); + using var manager = CreateManager(platform, health: true, port: 38471); + + Assert.IsFalse(Directory.Exists(Path.Combine(runtimeRoot, ".validate-stale"))); + Assert.IsFalse(Directory.Exists(Path.Combine(runtimeRoot, ".list-stale"))); + Assert.IsTrue(Directory.Exists(Path.Combine(runtimeRoot, "keep"))); + } + + [TestMethod] + public void DnsPolicyModeValuesRemainBackwardCompatible() + { + int[] expectedValues = [0, 1, 2, 3, 4]; + int[] actualValues = Enum.GetValues() + .Select(mode => (int)mode) + .ToArray(); + + CollectionAssert.AreEqual(expectedValues, actualValues); + } + + private DnsCryptPreparedComponent CreatePreparedComponent() + { + string preparedDirectory = Path.Combine(root, "prepared-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(preparedDirectory); + string preparedExecutable = Path.Combine(preparedDirectory, "dnscrypt-proxy.exe"); + File.WriteAllBytes(preparedExecutable, new byte[] { 0x4D, 0x5A }); + return new DnsCryptPreparedComponent(new Version(2, 1, 18), preparedDirectory, preparedExecutable); + } + + private static DnsCryptConfig CreateDirectDnsCryptConfig() + => new() + { + routeThroughShadowsocks = false, + }; + + private static DnsCryptRuntimeStartOptions CreateConcreteRuntimeOptions() + => new(new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = false, + serverNames = new List { "test-resolver" }, + }) + { + StaticResolverStamps = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["test-resolver"] = "sdns://test", + }, + }; + + private DnsCryptRuntimeManager CreateManager( + FakeRuntimePlatform platform, + bool health, + int port, + IReadOnlyList restartDelays = null, + IDnsCryptResolverCatalogBootstrapper bootstrapper = null) + { + return new DnsCryptRuntimeManager( + () => new DnsCryptComponentStatus(true, new Version(2, 1, 18), null, executablePath, true, null), + runtimeRoot, + platform, + () => port, + (_, _, _) => Task.FromResult(health), + restartDelays ?? [TimeSpan.Zero], + startupTimeout: TimeSpan.FromMilliseconds(40), + resolverCatalogBootstrapper: bootstrapper); + } + + private static async Task WaitUntilAsync(Func condition) + { + DateTime deadline = DateTime.UtcNow.AddSeconds(2); + while (!condition()) + { + if (DateTime.UtcNow >= deadline) + Assert.Fail("Timed out waiting for DNSCrypt runtime state change."); + await Task.Delay(10); + } + } + + private sealed class FakeCatalogBootstrapper : IDnsCryptResolverCatalogBootstrapper + { + public int CallCount { get; private set; } + public string LastHost { get; private set; } = string.Empty; + public int LastPort { get; private set; } + + public Task EnsureFreshAsync( + string cacheDirectory, + string shadowsocksHost, + int shadowsocksPort, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + CallCount++; + LastHost = shadowsocksHost; + LastPort = shadowsocksPort; + Directory.CreateDirectory(cacheDirectory); + File.WriteAllText(Path.Combine(cacheDirectory, "public-resolvers.md"), "verified resolver cache"); + File.WriteAllText(Path.Combine(cacheDirectory, "public-resolvers.md.minisig"), "verified resolver signature"); + return Task.CompletedTask; + } + } + + private sealed class FakeRuntimePlatform : IDnsCryptRuntimePlatform + { + public string VersionOutput { get; set; } = "2.1.18"; + public int CheckExitCode { get; set; } + public string CheckError { get; set; } = string.Empty; + public bool CheckThrowsTimeout { get; set; } + public string ListOutput { get; set; } = "[{\"name\":\"test-resolver\",\"proto\":\"DNSCrypt\",\"ipv6\":false,\"dnssec\":true,\"nolog\":true,\"nofilter\":true,\"description\":\"Test\",\"stamp\":\"sdns://test\"}]"; + public IReadOnlyList RuntimeOutputLines { get; set; } = Array.Empty(); + public bool ExitFirstProcessOnExitedSubscription { get; set; } + public bool WriteResolverCacheOnList { get; set; } + public List> Commands { get; } = new(); + public List> StartArguments { get; } = new(); + public List Processes { get; } = new(); + public bool ListCommandHadResolverCache { get; private set; } + public bool StartedWithResolverCache { get; private set; } + public string StartedConfigText { get; private set; } = string.Empty; + + public Task ExecuteAsync( + string executablePath, + string workingDirectory, + IReadOnlyList arguments, + TimeSpan timeout, + CancellationToken cancellationToken) + { + Commands.Add(arguments.ToArray()); + if (arguments.Contains("-list-all")) + { + ListCommandHadResolverCache |= + File.Exists(Path.Combine(workingDirectory, "public-resolvers.md")) + && File.Exists(Path.Combine(workingDirectory, "public-resolvers.md.minisig")); + if (WriteResolverCacheOnList) + { + File.WriteAllText(Path.Combine(workingDirectory, "public-resolvers.md"), "signed resolver cache"); + File.WriteAllText(Path.Combine(workingDirectory, "public-resolvers.md.minisig"), "signed resolver signature"); + } + } + if (arguments.Contains("-version")) + return Task.FromResult(new DnsCryptCommandResult(0, VersionOutput, string.Empty)); + if (arguments.Contains("-list-all")) + return Task.FromResult(new DnsCryptCommandResult(0, ListOutput, string.Empty)); + if (arguments.Contains("-check") && CheckThrowsTimeout) + throw new TimeoutException("Synthetic dnscrypt-proxy -check timeout."); + return Task.FromResult(new DnsCryptCommandResult(CheckExitCode, "Configuration successfully checked", CheckError)); + } + + public IDnsCryptRunningProcess Start( + string executablePath, + string workingDirectory, + IReadOnlyList arguments, + Action standardOutput, + Action standardError) + { + StartArguments.Add(arguments.ToArray()); + StartedWithResolverCache |= + File.Exists(Path.Combine(workingDirectory, "public-resolvers.md")) + && File.Exists(Path.Combine(workingDirectory, "public-resolvers.md.minisig")); + int configIndex = arguments.ToList().FindIndex(argument => string.Equals(argument, "-config", StringComparison.Ordinal)); + if (configIndex >= 0 && configIndex + 1 < arguments.Count && File.Exists(arguments[configIndex + 1])) + StartedConfigText = File.ReadAllText(arguments[configIndex + 1]); + var process = new FakeRunningProcess( + 7000 + Processes.Count, + ExitFirstProcessOnExitedSubscription && Processes.Count == 0); + Processes.Add(process); + standardOutput?.Invoke("[NOTICE] dnscrypt-proxy test fixture"); + foreach (string line in RuntimeOutputLines) + standardOutput?.Invoke(line); + return process; + } + } + + private sealed class FakeRunningProcess : IDnsCryptRunningProcess + { + private readonly bool exitOnExitedSubscription; + private EventHandler exited; + + public FakeRunningProcess(int id, bool exitOnExitedSubscription = false) + { + Id = id; + this.exitOnExitedSubscription = exitOnExitedSubscription; + } + + public int Id { get; } + public bool HasExited { get; private set; } + public bool WasKilled { get; private set; } + + public event EventHandler Exited + { + add + { + exited += value; + if (exitOnExitedSubscription && !HasExited) + { + HasExited = true; + value?.Invoke(this, EventArgs.Empty); + } + } + remove => exited -= value; + } + + public void TriggerUnexpectedExit() + { + if (HasExited) + return; + HasExited = true; + exited?.Invoke(this, EventArgs.Empty); + } + + public void Kill() + { + WasKilled = true; + if (!HasExited) + { + HasExited = true; + exited?.Invoke(this, EventArgs.Empty); + } + } + + public Task WaitForExitAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public void Dispose() { } + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptStage3Tests.cs b/Shadowsocks.UnitTests/DnsCryptStage3Tests.cs new file mode 100644 index 000000000..aea5cfeff --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptStage3Tests.cs @@ -0,0 +1,164 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Core; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptStage3Tests + { + [TestMethod] + public void FirstUseDefaultsEnableEverythingExceptIpv6() + { + DnsCryptConfig dns = new(); + + Assert.IsTrue(dns.autoUpdate); + Assert.IsTrue(dns.requireDnssec); + Assert.IsTrue(dns.requireNoLog); + Assert.IsTrue(dns.requireNoFilter); + Assert.IsTrue(dns.ipv4Servers); + Assert.IsFalse(dns.ipv6Servers); + Assert.IsTrue(dns.routeThroughShadowsocks); + Assert.IsTrue(dns.automaticResolvers); + Assert.IsTrue(dns.failClosed); + } + + [TestMethod] + public void OlderConfigurationWithoutDnsCryptSettingsIsRepaired() + { + string json = $"{{\"version\":\"{ApplicationInfo.Version}\",\"dnsPolicy\":{{\"mode\":0,\"customDohUrl\":\"\"}}}}"; + Assert.IsTrue(Configuration.TryDeserialize(json, out Configuration configuration)); + + Configuration.Process(ref configuration); + + Assert.IsNotNull(configuration.dnsPolicy); + Assert.IsNotNull(configuration.dnsPolicy.dnsCrypt); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.autoUpdate); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.failClosed); + Assert.IsNotNull(configuration.dnsPolicy.dnsCrypt.serverNames); + } + + + [TestMethod] + public void LegacyDnsCryptConfigurationDefaultsToAutomaticResolvers() + { + string json = $"{{\"version\":\"{ApplicationInfo.Version}\",\"dnsPolicy\":{{\"mode\":4,\"dnsCrypt\":{{\"serverNames\":[\"quad9-dnscrypt-ip4-nofilter-pri\",\"quad9-dnscrypt-ip4-nofilter-ecs-pri\"]}}}}}}"; + Assert.IsTrue(Configuration.TryDeserialize(json, out Configuration configuration)); + Configuration.Process(ref configuration); + + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.automaticResolvers); + Assert.AreEqual(0, configuration.dnsPolicy.dnsCrypt.serverNames.Count); + } + + [TestMethod] + public void DnsCryptModeAndSettingsRoundTripThroughConfigurationJson() + { + var source = new Configuration + { + dnsPolicy = new DnsPolicyConfig + { + mode = DnsPolicyMode.DnsCrypt, + directDnsServer = "9.9.9.9", + directDnsFallbackServer = "149.112.112.112", + directDnsRouteThroughShadowsocks = true, + customDohUrl = "https://dns.example/dns-query", + customDohRouteThroughShadowsocks = true, + dnsCrypt = new DnsCryptConfig + { + autoUpdate = false, + requireDnssec = true, + requireNoLog = false, + requireNoFilter = true, + ipv6Servers = true, + routeThroughShadowsocks = true, + automaticResolvers = false, + failClosed = false, + serverNames = ["cloudflare", "quad9-dnscrypt-ip4-filter-pri"], + }, + }, + }; + + string json = JsonConvert.SerializeObject(source); + Assert.IsTrue(Configuration.TryDeserialize(json, out Configuration restored)); + Configuration.Process(ref restored); + + Assert.AreEqual(DnsPolicyMode.DnsCrypt, restored.dnsPolicy.mode); + Assert.AreEqual("9.9.9.9", restored.dnsPolicy.directDnsServer); + Assert.AreEqual("149.112.112.112", restored.dnsPolicy.directDnsFallbackServer); + Assert.IsTrue(restored.dnsPolicy.directDnsRouteThroughShadowsocks); + Assert.AreEqual("https://dns.example/dns-query", restored.dnsPolicy.customDohUrl); + Assert.IsTrue(restored.dnsPolicy.customDohRouteThroughShadowsocks); + Assert.IsFalse(restored.dnsPolicy.dnsCrypt.autoUpdate); + Assert.IsTrue(restored.dnsPolicy.dnsCrypt.requireNoFilter); + Assert.IsTrue(restored.dnsPolicy.dnsCrypt.ipv6Servers); + Assert.IsTrue(restored.dnsPolicy.dnsCrypt.routeThroughShadowsocks); + Assert.IsFalse(restored.dnsPolicy.dnsCrypt.automaticResolvers); + Assert.IsFalse(restored.dnsPolicy.dnsCrypt.failClosed); + CollectionAssert.AreEqual(new[] { "cloudflare", "quad9-dnscrypt-ip4-filter-pri" }, restored.dnsPolicy.dnsCrypt.serverNames); + } + + [TestMethod] + public void TrayPresentationCoversAllDnsCryptManagementStates() + { + Version installed = new(2, 1, 18); + DnsCryptComponentStatus component = new(true, installed, null, @"C:\\dnscrypt-proxy.exe", true, null); + + AssertTray("DNSCrypt: Not installed", Status(false, null, DnsCryptRuntimeState.NotInstalled)); + AssertTray("DNSCrypt: Starting…", Status(true, installed, DnsCryptRuntimeState.Starting)); + AssertTray("DNSCrypt: Updating…", Status(true, installed, DnsCryptRuntimeState.Updating)); + AssertTray("DNSCrypt: Running · {0}", Status(true, installed, DnsCryptRuntimeState.Running), installed); + AssertTray("DNSCrypt: Failed", Status(true, installed, DnsCryptRuntimeState.Failed)); + AssertTray("DNSCrypt: Stopped · {0}", Status(true, installed, DnsCryptRuntimeState.Stopped), installed); + + DnsCryptManagementStatus update = new( + DnsPolicyMode.DnsCrypt, + component, + new DnsCryptRuntimeStatus(DnsCryptRuntimeState.Running, installed, 1, 5300, DateTimeOffset.UtcNow, "config", null), + new Version(2, 1, 19)); + AssertTray("DNSCrypt: Update available · {0}", update, new Version(2, 1, 19)); + + DnsCryptManagementStatus updatingWithKnownRelease = update with + { + Runtime = new DnsCryptRuntimeStatus(DnsCryptRuntimeState.Updating, installed, 1, 5300, DateTimeOffset.UtcNow, "config", null), + }; + AssertTray("DNSCrypt: Updating…", updatingWithKnownRelease); + } + + [TestMethod] + public void UpdateAvailableRequiresNewerKnownVersion() + { + Version installed = new(2, 1, 18); + DnsCryptComponentStatus component = new(true, installed, null, "dnscrypt-proxy.exe", true, null); + DnsCryptRuntimeStatus runtime = new(DnsCryptRuntimeState.Stopped, installed, 0, 0, null, "config", null); + + Assert.IsFalse(new DnsCryptManagementStatus(DnsPolicyMode.System, component, runtime, null).UpdateAvailable); + Assert.IsFalse(new DnsCryptManagementStatus(DnsPolicyMode.System, component, runtime, installed).UpdateAvailable); + Assert.IsTrue(new DnsCryptManagementStatus(DnsPolicyMode.System, component, runtime, new Version(2, 1, 19)).UpdateAvailable); + } + + private static DnsCryptManagementStatus Status(bool installed, Version version, DnsCryptRuntimeState state) + { + DnsCryptComponentStatus component = new(installed, installed ? version : null, null, installed ? "dnscrypt-proxy.exe" : null, true, null); + DnsCryptRuntimeStatus runtime = new( + state, + version, + state is DnsCryptRuntimeState.Running or DnsCryptRuntimeState.Updating ? 123 : 0, + state is DnsCryptRuntimeState.Running or DnsCryptRuntimeState.Updating ? 5300 : 0, + null, + "config", + null); + return new DnsCryptManagementStatus(DnsPolicyMode.DnsCrypt, component, runtime, null); + } + + private static void AssertTray(string key, DnsCryptManagementStatus status, Version version = null) + { + DnsCryptTrayPresentation presentation = DnsCryptPresentation.GetTrayPresentation(status); + Assert.AreEqual(key, presentation.LocalizationKey); + Assert.AreEqual(version, presentation.Version); + } + } +} diff --git a/Shadowsocks.UnitTests/DnsCryptTomlGeneratorTests.cs b/Shadowsocks.UnitTests/DnsCryptTomlGeneratorTests.cs new file mode 100644 index 000000000..a2880e4f2 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsCryptTomlGeneratorTests.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsCryptTomlGeneratorTests + { + [TestMethod] + public void RuntimeRequiresConcreteSelectedResolver() + { + Assert.ThrowsExactly(() => + DnsCryptTomlGenerator.Generate(38471, new DnsCryptConfig { routeThroughShadowsocks = false })); + } + + [TestMethod] + public void DnsCryptAndDohResolversAreEnabledWhileOdohRemainsDisabled() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + serverNames = new List { "cloudflare" }, + }; + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "dnscrypt_servers = true"); + StringAssert.Contains(toml, "doh_servers = true"); + StringAssert.Contains(toml, "odoh_servers = false"); + string[] tomlLines = toml.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + Assert.IsFalse(Array.Exists(tomlLines, static line => string.Equals(line, "doh_servers = false", StringComparison.Ordinal))); + } + + [TestMethod] + public void ProxyRoutingForcesTcpAndWritesLocalSocksEndpoint() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = true, + serverNames = new List { "cloudflare" }, + }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + 1080, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "force_tcp = true"); + StringAssert.Contains(toml, "proxy = 'socks5://127.0.0.1:1080'"); + } + + [TestMethod] + public void SelectedServersAreTrimmedAndDeduplicated() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = false, + serverNames = new List { " cloudflare ", "quad9-dnscrypt-ip4-filter-pri", "CLOUDFLARE" }, + }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "server_names = ['cloudflare', 'quad9-dnscrypt-ip4-filter-pri']"); + } + + + [TestMethod] + public void AutomaticRuntimeNeverFallsBackToStaticCloudflare() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = true, + serverNames = new List { "quad9-dnscrypt-ip4-filter-pri" }, + }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "server_names = ['quad9-dnscrypt-ip4-filter-pri']"); + StringAssert.Contains(toml, "bootstrap_resolvers = []"); + StringAssert.Contains(toml, "[static.'quad9-dnscrypt-ip4-filter-pri']"); + Assert.IsFalse(toml.Contains("[sources.public-resolvers]", StringComparison.Ordinal)); + Assert.IsFalse(DnsCryptTomlGenerator.RuntimeUsesPlaintextBootstrap(toml)); + } + + + [TestMethod] + public void AutomaticRuntimeUsesSelectedFilteredNamesWhenProvided() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = true, + serverNames = new List { "dnscry.pt-moscow-ipv4" }, + }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "server_names = ['dnscry.pt-moscow-ipv4']"); + StringAssert.Contains(toml, "[static.'dnscry.pt-moscow-ipv4']"); + StringAssert.Contains(toml, "bootstrap_resolvers = []"); + Assert.IsFalse(toml.Contains("[sources.public-resolvers]", StringComparison.Ordinal)); + Assert.IsFalse(DnsCryptTomlGenerator.RuntimeUsesPlaintextBootstrap(toml)); + } + + [TestMethod] + public void ManualRuntimeUsesPinnedSignedStampsWithoutPlaintextBootstrap() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = false, + serverNames = new List { "cloudflare", "quad9-dnscrypt-ip4-filter-pri" }, + }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: Stamps(config.serverNames)); + + StringAssert.Contains(toml, "bootstrap_resolvers = []"); + StringAssert.Contains(toml, "[static.'cloudflare']"); + StringAssert.Contains(toml, "[static.'quad9-dnscrypt-ip4-filter-pri']"); + Assert.IsFalse(toml.Contains("[sources.public-resolvers]", StringComparison.Ordinal)); + Assert.IsFalse(DnsCryptTomlGenerator.RuntimeUsesPlaintextBootstrap(toml)); + } + + [TestMethod] + public void ResolverCatalogUsesPreseededCacheWithoutPlaintextBootstrap() + { + var config = new DnsCryptConfig { automaticResolvers = false }; + + string toml = DnsCryptTomlGenerator.Generate( + 5300, + config, + purpose: DnsCryptTomlPurpose.ResolverCatalog); + + StringAssert.Contains(toml, "bootstrap_resolvers = []"); + StringAssert.Contains(toml, "ignore_system_dns = true"); + StringAssert.Contains(toml, "force_tcp = false"); + Assert.IsFalse(toml.Contains("proxy = 'socks5://", StringComparison.Ordinal)); + StringAssert.Contains(toml, "[sources.public-resolvers]"); + StringAssert.Contains(toml, "urls = []"); + StringAssert.Contains(toml, "cache_ttl = 168"); + Assert.IsFalse(toml.Contains("raw.githubusercontent.com", StringComparison.Ordinal)); + Assert.IsFalse(toml.Contains("download.dnscrypt.info", StringComparison.Ordinal)); + Assert.IsFalse(toml.Contains("cdn.jsdelivr.net", StringComparison.Ordinal)); + Assert.IsFalse(DnsCryptTomlGenerator.RuntimeUsesPlaintextBootstrap(toml)); + } + + [TestMethod] + public void ActiveRuntimeRejectsResolverWithoutSignedStamp() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + serverNames = new List { "cloudflare" }, + }; + + Assert.ThrowsExactly(() => + DnsCryptTomlGenerator.Generate( + 5300, + config, + staticResolverStamps: new Dictionary(StringComparer.OrdinalIgnoreCase))); + } + + [TestMethod] + public void InvalidServerNameIsRejectedInsteadOfBeingInjectedIntoToml() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = false, + automaticResolvers = false, + serverNames = new List { "good', proxy='evil" }, + }; + + Assert.ThrowsExactly(() => DnsCryptTomlGenerator.Generate(5300, config)); + } + + [TestMethod] + public void ProxyRoutingRequiresSocksPort() + { + var config = new DnsCryptConfig + { + routeThroughShadowsocks = true, + serverNames = new List { "cloudflare" }, + }; + + Assert.ThrowsExactly(() => DnsCryptTomlGenerator.Generate(5300, config)); + } + + [TestMethod] + public void AtLeastOneAddressFamilyMustBeEnabled() + { + var config = new DnsCryptConfig { ipv4Servers = false, ipv6Servers = false, routeThroughShadowsocks = false }; + + Assert.ThrowsExactly(() => DnsCryptTomlGenerator.Generate(5300, config)); + } + + private static Dictionary Stamps(IEnumerable names) + => names + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToDictionary(name => name, name => $"sdns://{name}", StringComparer.OrdinalIgnoreCase); + } +} diff --git a/Shadowsocks.UnitTests/DnsProcessResolverTests.cs b/Shadowsocks.UnitTests/DnsProcessResolverTests.cs new file mode 100644 index 000000000..b7aad2532 --- /dev/null +++ b/Shadowsocks.UnitTests/DnsProcessResolverTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.NetworkService.Routing; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class DnsProcessResolverTests + { + [TestMethod] + public void DnsOwnerLookupRetriesShortLivedSocketRegistration() + { + var delays = new List(); + int calls = 0; + int pid = ProcessResolver.ResolvePidWithRetry( + () => ++calls == 4 ? 4242 : 0, + dnsFlow: true, + delays.Add); + + Assert.AreEqual(4242, pid); + Assert.AreEqual(4, calls); + CollectionAssert.AreEqual(new[] { 1, 4, 10 }, delays); + } + + [TestMethod] + public void NonDnsOwnerLookupDoesNotRetry() + { + int calls = 0; + int pid = ProcessResolver.ResolvePidWithRetry( + () => + { + calls++; + return 0; + }, + dnsFlow: false, + _ => Assert.Fail("Non-DNS owner lookup must not delay.")); + + Assert.AreEqual(0, pid); + Assert.AreEqual(1, calls); + } + } +} diff --git a/Shadowsocks.UnitTests/EmbeddedLegalResourcesTests.cs b/Shadowsocks.UnitTests/EmbeddedLegalResourcesTests.cs new file mode 100644 index 000000000..291a2e12d --- /dev/null +++ b/Shadowsocks.UnitTests/EmbeddedLegalResourcesTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Core; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class EmbeddedLegalResourcesTests +{ + [TestMethod] + public void ProductLicenseIsEmbeddedAndCurrent() + { + string text = EmbeddedResources.ProductLicense; + + StringAssert.Contains(text, "Shadowsocks Reborn"); + StringAssert.Contains(text, "GNU GENERAL PUBLIC LICENSE"); + StringAssert.Contains(text, "Version 3, 29 June 2007"); + StringAssert.Contains(text, "GPL-3.0-or-later"); + Assert.IsFalse(text.Contains("Privoxy", System.StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(text.Contains("mbed TLS", System.StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void ThirdPartyNoticesAreEmbeddedAndCoverRuntimeDependencies() + { + string text = EmbeddedResources.ThirdPartyNotices; + string[] required = + [ + "Bouncy Castle Cryptography for .NET", + "Google.Protobuf", + "Newtonsoft.Json", + "NLog", + "WinUIEx", + "ZXing.Net", + "System.Management", + "System.Drawing.Common", + "Windows App SDK", + "dnscrypt-proxy", + "WinDivert", + "ByteCircularBuffer", + "Adblock Plus-derived network routing", + ]; + + foreach (string component in required) + { + StringAssert.Contains(text, component); + } + } + + + [TestMethod] + public void LegalViewerUsesSelectedUiCultureAndPreservesAuthoritativeOriginalTerms() + { + (string Culture, string LicensePrefix, string NoticesPrefix)[] cases = + [ + ("en-US", "Shadowsocks Reborn License", "Third-party component notices"), + ("ru-RU", "Лицензия Shadowsocks Reborn", "Уведомления сторонних компонентов"), + ("zh-CN", "Shadowsocks Reborn 许可证", "第三方组件声明"), + ("zh-TW", "Shadowsocks Reborn 授權條款", "第三方元件聲明"), + ("ja-JP", "Shadowsocks Reborn ライセンス", "サードパーティコンポーネント通知"), + ("ko-KR", "Shadowsocks Reborn 라이선스", "서드파티 구성 요소 고지"), + ("fr-FR", "Licence de Shadowsocks Reborn", "Mentions relatives aux composants tiers"), + ]; + + foreach ((string cultureName, string licensePrefix, string noticesPrefix) in cases) + { + CultureInfo culture = CultureInfo.GetCultureInfo(cultureName); + string license = EmbeddedResources.LocalizedProductLicense(culture); + string notices = EmbeddedResources.LocalizedThirdPartyNotices(culture); + + Assert.IsTrue(license.StartsWith(licensePrefix, StringComparison.Ordinal), cultureName); + Assert.IsTrue(notices.StartsWith(noticesPrefix, StringComparison.Ordinal), cultureName); + StringAssert.Contains(license, "GNU GENERAL PUBLIC LICENSE"); + StringAssert.Contains(license, "Version 3, 29 June 2007"); + StringAssert.Contains(notices, "Bouncy Castle Cryptography for .NET"); + StringAssert.Contains(notices, "Adblock Plus-derived network routing"); + Assert.IsFalse(notices.StartsWith("# Third-party notices", StringComparison.Ordinal)); + } + } + +} diff --git a/Shadowsocks.UnitTests/FilterEngineTests.cs b/Shadowsocks.UnitTests/FilterEngineTests.cs new file mode 100644 index 000000000..875aeed30 --- /dev/null +++ b/Shadowsocks.UnitTests/FilterEngineTests.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Routing; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class FilterEngineTests + { + [TestMethod] + public void BlockingAndExceptionRulesMapToProxyAndDirect() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "||blocked.example^", "@@||direct.example^" }, + Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://blocked.example/a", "blocked.example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://direct.example/a", "direct.example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://other.example/a", "other.example").Action); + } + + [TestMethod] + public void UserRulesHavePriorityOverGeneratedRules() + { + FilterEngine proxyOverride = FilterEngine.Compile( + new[] { "@@||example.com^" }, + new[] { "||example.com^" }); + FilterEngine directOverride = FilterEngine.Compile( + new[] { "||example.com^" }, + new[] { "@@||example.com^" }); + + FilterRoutingDecision proxy = proxyOverride.Evaluate("https://example.com/", "example.com"); + FilterRoutingDecision direct = directOverride.Evaluate("https://example.com/", "example.com"); + + Assert.AreEqual(FilterRoutingAction.Proxy, proxy.Action); + Assert.AreEqual(FilterRuleSource.User, proxy.Source); + Assert.AreEqual(FilterRoutingAction.Direct, direct.Action); + Assert.AreEqual(FilterRuleSource.User, direct.Source); + } + + [TestMethod] + public void ExceptionWinsWithinSameRuleSet() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "||example.com^", "@@||example.com/safe^" }, + Array.Empty()); + + FilterRoutingDecision decision = engine.Evaluate("https://example.com/safe/file.js", "example.com"); + + Assert.AreEqual(FilterRoutingAction.Direct, decision.Action); + Assert.AreEqual("@@||example.com/safe^", decision.MatchedRule); + } + + [TestMethod] + public void DomainAnchorMatchesSubdomainsButNotLookalikes() + { + FilterEngine engine = FilterEngine.Compile(new[] { "||example.com^" }, Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://example.com/a", "example.com").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://cdn.example.com/a", "cdn.example.com").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://notexample.com/a", "notexample.com").Action); + } + + [TestMethod] + public void HostCanBeDerivedFromAbsoluteUrl() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "||derived.example^" }, + Array.Empty()); + + FilterRoutingDecision decision = engine.Evaluate("https://cdn.derived.example/path"); + + Assert.AreEqual(FilterRoutingAction.Proxy, decision.Action); + Assert.AreEqual(FilterRuleSource.Default, decision.Source); + } + + [TestMethod] + public void SimpleDomainRulesUseOptimizedHostSuffixPath() + { + _ = FilterEngine.Compile( + new[] { "||one.example^", "||two.example", "/complex.*/" }, + Array.Empty(), + out FilterCompilationReport report); + + Assert.AreEqual(3, report.DefaultRuleCount); + Assert.AreEqual(2, report.OptimizedDomainRuleCount); + Assert.AreEqual(1, report.UnindexedRuleCount); + } + + [TestMethod] + public void FullAnchorAndWildcardFollowAbpNetworkFilterSemantics() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "|https://exact.example/path|", "ads.example/*/banner" }, + Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://exact.example/path", "exact.example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://exact.example/path/more", "exact.example").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://ads.example/a/b/banner", "ads.example").Action); + } + + [TestMethod] + public void ConsecutiveWildcardsPreserveAbpNetworkFilterSemantics() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "ads.example/**/banner" }, + Array.Empty()); + + Assert.AreEqual( + FilterRoutingAction.Proxy, + engine.Evaluate("https://ads.example/a/b/banner", "ads.example").Action); + } + + [TestMethod] + public void ExplicitRegexRulesAreSupported() + { + FilterEngine engine = FilterEngine.Compile(new[] { @"/^https?:\/\/([^\/]+\.)?regex\.example\/.*$/" }, Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://cdn.regex.example/file", "cdn.regex.example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://regex.invalid/file", "regex.invalid").Action); + } + + [TestMethod] + public void DomainOptionSupportsIncludesExcludesAndParentDomains() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "/ads/$domain=example.com|~safe.example.com" }, + Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://cdn.test/ads/x", "www.example.com").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://cdn.test/ads/x", "safe.example.com").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://cdn.test/ads/x", "child.safe.example.com").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://cdn.test/ads/x", "other.test").Action); + } + + [TestMethod] + public void ShadowsocksDomainRuleNormalizationKeepsOptionsValid() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "||example.com$domain=example.com" }, + Array.Empty(), + out FilterCompilationReport report); + + Assert.AreEqual(0, report.InvalidRuleCount); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://example.com/path", "example.com").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://example.com.evil/path", "example.com.evil").Action); + } + + [TestMethod] + public void MatchCaseOptionIsHonored() + { + FilterEngine engine = FilterEngine.Compile(new[] { "CaseSensitive$match-case" }, Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://example/CaseSensitive", "example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://example/casesensitive", "example").Action); + } + + [TestMethod] + public void CompatibilityContentOptionsDoNotInvalidateNetworkRule() + { + FilterEngine engine = FilterEngine.Compile(new[] { "||example.com^$script,third-party" }, Array.Empty(), out FilterCompilationReport report); + + Assert.AreEqual(0, report.InvalidRuleCount); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://example.com/app.js", "example.com").Action); + } + + [TestMethod] + public void SiteKeyRulesRemainInactiveInNetworkOnlyRouting() + { + FilterEngine engine = FilterEngine.Compile(new[] { "||example.com^$sitekey=ABC123" }, Array.Empty(), out FilterCompilationReport report); + + Assert.AreEqual(0, report.InvalidRuleCount); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://example.com/", "example.com").Action); + } + + [TestMethod] + public void UnsupportedOptionsAreReportedAndIgnored() + { + FilterEngine engine = FilterEngine.Compile( + new[] { "||bad.example^$made-up-option", "||good.example^" }, + Array.Empty(), + out FilterCompilationReport report); + + Assert.AreEqual(1, report.InvalidRuleCount); + StringAssert.Contains(report.InvalidRules[0].Reason, "unsupported option"); + Assert.AreEqual(FilterRoutingAction.Direct, engine.Evaluate("https://bad.example/", "bad.example").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("https://good.example/", "good.example").Action); + } + + [TestMethod] + public void ParseRuleLinesMatchesUserRuleFileContract() + { + string content = "! comment\r\n[Adblock Plus 2.0]\r\n\r\n||proxy.example^\r\n@@||direct.example^\r\n"; + + IReadOnlyList rules = FilterEngine.ParseRuleLines(content); + + CollectionAssert.AreEqual( + new[] { "||proxy.example^", "@@||direct.example^" }, + new List(rules)); + } + + [TestMethod] + public void GeneratedGeositeRuleShapesAreCompatible() + { + FilterEngine blacklist = FilterEngine.Compile( + new[] { "plain-token", "||domain.example", "|http://full.example", "|https://full.example", @"/regex\d+\.example/" }, + Array.Empty()); + + Assert.AreEqual(FilterRoutingAction.Proxy, blacklist.Evaluate("https://site/plain-token/path", "site").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, blacklist.Evaluate("https://cdn.domain.example/path", "cdn.domain.example").Action); + Assert.AreEqual(FilterRoutingAction.Direct, blacklist.Evaluate("https://domain.example.evil/path", "domain.example.evil").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, blacklist.Evaluate("https://full.example/path", "full.example").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, blacklist.Evaluate("https://regex12.example/path", "regex12.example").Action); + + FilterEngine whitelist = FilterEngine.Compile(new[] { "/.*/", "@@||direct.example" }, Array.Empty()); + Assert.AreEqual(FilterRoutingAction.Direct, whitelist.Evaluate("https://direct.example/", "direct.example").Action); + Assert.AreEqual(FilterRoutingAction.Proxy, whitelist.Evaluate("https://other.example/", "other.example").Action); + } + + [TestMethod] + public void PrivateIpv4RangesBypassRules() + { + FilterEngine engine = FilterEngine.Compile(new[] { "/.*/" }, Array.Empty()); + + Assert.AreEqual(FilterRuleSource.PrivateNetwork, engine.Evaluate("http://10.0.0.5/", "10.0.0.5").Source); + Assert.AreEqual(FilterRuleSource.PrivateNetwork, engine.Evaluate("http://127.0.0.1/", "127.0.0.1").Source); + Assert.AreEqual(FilterRuleSource.PrivateNetwork, engine.Evaluate("http://172.16.5.4/", "172.16.5.4").Source); + Assert.AreEqual(FilterRuleSource.PrivateNetwork, engine.Evaluate("http://192.168.1.2/", "192.168.1.2").Source); + Assert.AreEqual(FilterRoutingAction.Proxy, engine.Evaluate("http://8.8.8.8/", "8.8.8.8").Action); + } + + [TestMethod] + public void EmbeddedDefaultUserRuleFileCompilesWithoutInvalidRules() + { + IReadOnlyList rules = FilterEngine.ParseRuleLines(Shadowsocks.Core.EmbeddedResources.UserRule); + _ = FilterEngine.Compile(Array.Empty(), rules, out FilterCompilationReport report); + + Assert.AreEqual(0, report.InvalidRuleCount); + Assert.AreEqual(0, report.UserRuleCount); + } + } +} diff --git a/Shadowsocks.UnitTests/GeositeSourceTest.cs b/Shadowsocks.UnitTests/GeositeSourceTest.cs new file mode 100644 index 000000000..630ebcf2e --- /dev/null +++ b/Shadowsocks.UnitTests/GeositeSourceTest.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class GeositeSourceTest + { + [TestMethod] + public void ChecksumUrlIsAdjacentToDatabase() + { + const string source = "https://raw.githubusercontent.com/runetfreedom/russia-blocked-geosite/release/geosite.dat"; + Assert.AreEqual(source + ".sha256sum", GeositeUpdater.GetChecksumUrl(source)); + } + + [TestMethod] + public void GeositeSourcesAreTrimmedAndDeduplicated() + { + var sources = Configuration.NormalizeGeositeSourceList(new List + { + " https://example.com/a.dat ", + "https://example.com/a.dat", + "https://example.com/b.dat", + }); + + CollectionAssert.AreEqual(new List + { + "https://example.com/a.dat", + "https://example.com/b.dat", + }, sources); + } + } +} diff --git a/Shadowsocks.UnitTests/HotkeyGestureTests.cs b/Shadowsocks.UnitTests/HotkeyGestureTests.cs new file mode 100644 index 000000000..63c46fa28 --- /dev/null +++ b/Shadowsocks.UnitTests/HotkeyGestureTests.cs @@ -0,0 +1,29 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Hotkeys; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class HotkeyGestureTests +{ + [TestMethod] + [DataRow("Ctrl+A", "Ctrl+A")] + [DataRow("Ctrl+Alt+D2", "Ctrl+Alt+D2")] + [DataRow("Ctrl+Alt+Shift+NumPad7", "Ctrl+Alt+Shift+NumPad7")] + [DataRow("Ctrl+Shift+Alt+F6", "Ctrl+Alt+Shift+F6")] + public void HotkeyGestureRoundTrips(string input, string canonical) + { + Assert.IsTrue(HotkeyGesture.TryParse(input, out HotkeyGesture gesture)); + Assert.AreEqual(canonical, gesture.ToString()); + } + + [TestMethod] + [DataRow("")] + [DataRow("A")] + [DataRow("Ctrl+")] + [DataRow("Ctrl+DefinitelyNotAKey")] + public void InvalidHotkeyGestureIsRejected(string input) + { + Assert.IsFalse(HotkeyGesture.TryParse(input, out _)); + } +} diff --git a/Shadowsocks.UnitTests/HttpProxyTests.cs b/Shadowsocks.UnitTests/HttpProxyTests.cs new file mode 100644 index 000000000..f7fc6f01f --- /dev/null +++ b/Shadowsocks.UnitTests/HttpProxyTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Core; +using Shadowsocks.Proxy; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class HttpProxyTests + { + [TestMethod] + public async Task ConnectUsesProductUserAgentBasicAuthAndPreservesTunnelBytes() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + string request = await ReadHeadersAsync(stream); + + StringAssert.Contains(request, "CONNECT example.com:443 HTTP/1.1\r\n"); + StringAssert.Contains(request, $"User-Agent: Shadowsocks-Reborn/{ApplicationInfo.Version}\r\n"); + string expectedAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes("user:secret")); + StringAssert.Contains(request, $"Proxy-Authorization: Basic {expectedAuth}\r\n"); + + byte[] responsePrefix = Encoding.ASCII.GetBytes( + "HTTP/1.1 200 Connection established\r\nProxy-Agent: test\r\n"); + foreach (byte value in responsePrefix) + { + await stream.WriteAsync(new byte[] { value }); + await Task.Yield(); + } + await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\nHELLO")); + }); + + var proxy = new HttpProxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await ConnectDestinationAsync( + proxy, + new DnsEndPoint("example.com", 443), + new NetworkCredential("user", "secret")); + + var payload = new byte[5]; + int received = await ReceiveAsync(proxy, payload); + Assert.AreEqual(5, received); + Assert.AreEqual("HELLO", Encoding.ASCII.GetString(payload)); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + [TestMethod] + public async Task Non200ConnectResponseFails() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + await ReadHeadersAsync(stream); + await stream.WriteAsync(Encoding.ASCII.GetBytes( + "HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n")); + }); + + var proxy = new HttpProxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await Assert.ThrowsExactlyAsync(() => + ConnectDestinationAsync(proxy, new DnsEndPoint("example.com", 443), null)); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + private static Task ConnectProxyAsync(HttpProxy proxy, EndPoint endpoint) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + proxy.BeginConnectProxy(endpoint, ar => + { + try + { + proxy.EndConnectProxy(ar); + completion.TrySetResult(); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + return completion.Task; + } + + private static Task ConnectDestinationAsync(HttpProxy proxy, EndPoint endpoint, NetworkCredential credential) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + proxy.BeginConnectDest(endpoint, ar => + { + try + { + proxy.EndConnectDest(ar); + completion.TrySetResult(); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null, credential); + return completion.Task; + } + + private static Task ReceiveAsync(HttpProxy proxy, byte[] buffer) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + proxy.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ar => + { + try + { + completion.TrySetResult(proxy.EndReceive(ar)); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + return completion.Task; + } + + private static async Task ReadHeadersAsync(NetworkStream stream) + { + var bytes = new System.Collections.Generic.List(); + var one = new byte[1]; + while (bytes.Count < 64 * 1024) + { + int read = await stream.ReadAsync(one); + if (read == 0) + { + throw new InvalidOperationException("Unexpected EOF while reading HTTP CONNECT request."); + } + bytes.Add(one[0]); + int count = bytes.Count; + if (count >= 4 + && bytes[count - 4] == '\r' + && bytes[count - 3] == '\n' + && bytes[count - 2] == '\r' + && bytes[count - 1] == '\n') + { + return Encoding.ASCII.GetString(bytes.ToArray()); + } + } + + throw new InvalidOperationException("HTTP CONNECT request headers exceeded the test limit."); + } + } +} diff --git a/Shadowsocks.UnitTests/IpFragmentTrackerTests.cs b/Shadowsocks.UnitTests/IpFragmentTrackerTests.cs new file mode 100644 index 000000000..d3f05a664 --- /dev/null +++ b/Shadowsocks.UnitTests/IpFragmentTrackerTests.cs @@ -0,0 +1,172 @@ +#nullable enable +using System; +using System.Buffers.Binary; +using System.Net; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.Routing; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class IpFragmentTrackerTests + { + private static readonly DateTime Now = new(2026, 8, 18, 12, 0, 0, DateTimeKind.Utc); + + [TestMethod] + public void ParsesIpv4FirstAndContinuationFragments() + { + byte[] first = CreateIpv4Fragment(id: 0x1234, offset: 0, moreFragments: true); + byte[] last = CreateIpv4Fragment(id: 0x1234, offset: 185, moreFragments: false); + + Assert.IsTrue(IpFragmentTracker.TryParse(first, out IpFragmentInfo firstInfo)); + Assert.IsTrue(firstInfo.IsFirstFragment); + Assert.IsTrue(firstInfo.MoreFragments); + Assert.AreEqual((byte)17, firstInfo.Key.Protocol); + Assert.AreEqual(IPAddress.Parse("192.0.2.10"), firstInfo.Key.SourceAddress); + Assert.AreEqual(IPAddress.Parse("198.51.100.53"), firstInfo.Key.DestinationAddress); + Assert.AreEqual((uint)0x1234, firstInfo.Key.Identification); + + Assert.IsTrue(IpFragmentTracker.TryParse(last, out IpFragmentInfo lastInfo)); + Assert.IsFalse(lastInfo.IsFirstFragment); + Assert.IsFalse(lastInfo.MoreFragments); + Assert.AreEqual(firstInfo.Key, lastInfo.Key); + } + + [TestMethod] + public void ParsesIpv6FragmentHeaderAfterExtensionHeader() + { + byte[] packet = CreateIpv6Fragment(id: 0x10203040, offset: 0, moreFragments: true); + + Assert.IsTrue(IpFragmentTracker.TryParse(packet, out IpFragmentInfo info)); + Assert.IsTrue(info.IsFirstFragment); + Assert.AreEqual((byte)17, info.Key.Protocol); + Assert.AreEqual((uint)0x10203040, info.Key.Identification); + Assert.AreEqual(IPAddress.Parse("2001:db8::1"), info.Key.SourceAddress); + Assert.AreEqual(IPAddress.Parse("2001:db8::53"), info.Key.DestinationAddress); + } + + [TestMethod] + public void Ipv6FragmentCanPointToPostFragmentExtensionHeader() + { + byte[] packet = new byte[64]; + packet[0] = 0x60; + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(4, 2), 24); + packet[6] = 44; // Fragment + packet[7] = 64; + IPAddress.Parse("2001:db8::1").GetAddressBytes().CopyTo(packet, 8); + IPAddress.Parse("2001:db8::53").GetAddressBytes().CopyTo(packet, 24); + packet[40] = 60; // Destination Options follows Fragment + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(42, 2), 0x0001); + BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(44, 4), 0x11223344); + packet[48] = 17; // UDP follows Destination Options + packet[49] = 0; // 8-byte Destination Options header + + Assert.IsTrue(IpFragmentTracker.TryParse(packet, out IpFragmentInfo fragment)); + Assert.IsTrue(fragment.IsFirstFragment); + Assert.IsTrue(fragment.MoreFragments); + Assert.AreEqual((byte)60, fragment.Key.Protocol); + Assert.AreEqual(0x11223344u, fragment.Key.Identification); + } + + [TestMethod] + public void AtomicIpv6FragmentDoesNotRequireFragmentTracking() + { + byte[] packet = CreateIpv6Fragment(id: 7, offset: 0, moreFragments: false); + Assert.IsFalse(IpFragmentTracker.TryParse(packet, out _)); + } + + [TestMethod] + public void TrackerCarriesDirectDecisionAcrossFragmentsAndRemovesOnLast() + { + IpFragmentInfo first = Parse(CreateIpv4Fragment(9, 0, moreFragments: true)); + IpFragmentInfo last = Parse(CreateIpv4Fragment(9, 100, moreFragments: false)); + var tracker = new IpFragmentTracker(); + + tracker.Record(first, FragmentDisposition.Direct, Now); + Assert.IsTrue(tracker.TryGetDisposition(last, Now.AddMilliseconds(1), out FragmentDisposition disposition)); + Assert.AreEqual(FragmentDisposition.Direct, disposition); + Assert.IsFalse(tracker.TryGetDisposition(last, Now.AddMilliseconds(2), out _)); + } + + [TestMethod] + public void UnknownContinuationFailsSecure() + { + IpFragmentInfo continuation = Parse(CreateIpv4Fragment(17, 100, moreFragments: false)); + var tracker = new IpFragmentTracker(); + + Assert.IsFalse(tracker.TryGetDisposition(continuation, Now, out FragmentDisposition disposition)); + Assert.AreEqual(FragmentDisposition.Drop, disposition); + } + + [TestMethod] + public void TransparentRoutesDropFragmentedDatagrams() + { + Assert.AreEqual( + FragmentDisposition.Drop, + IpFragmentTracker.ForDnsDecision(DnsRouteDecision.Redirect)); + Assert.AreEqual( + FragmentDisposition.Drop, + IpFragmentTracker.ForDnsDecision(DnsRouteDecision.Block)); + Assert.AreEqual( + FragmentDisposition.Direct, + IpFragmentTracker.ForDnsDecision(DnsRouteDecision.Direct)); + Assert.AreEqual(FragmentDisposition.Drop, IpFragmentTracker.ForRoute(RouteAction.Proxy)); + Assert.AreEqual(FragmentDisposition.Drop, IpFragmentTracker.ForRoute(RouteAction.Block)); + Assert.AreEqual(FragmentDisposition.Direct, IpFragmentTracker.ForRoute(RouteAction.Direct)); + } + + private static IpFragmentInfo Parse(byte[] packet) + { + Assert.IsTrue(IpFragmentTracker.TryParse(packet, out IpFragmentInfo info)); + return info; + } + + private static byte[] CreateIpv4Fragment(ushort id, int offset, bool moreFragments) + { + byte[] packet = new byte[28]; + packet[0] = 0x45; + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(2, 2), checked((ushort)packet.Length)); + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(4, 2), id); + ushort flagsAndOffset = checked((ushort)(offset & 0x1FFF)); + if (moreFragments) + flagsAndOffset |= 0x2000; + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(6, 2), flagsAndOffset); + packet[8] = 64; + packet[9] = 17; + IPAddress.Parse("192.0.2.10").GetAddressBytes().CopyTo(packet, 12); + IPAddress.Parse("198.51.100.53").GetAddressBytes().CopyTo(packet, 16); + if (offset == 0) + { + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(20, 2), 53000); + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(22, 2), 53); + } + return packet; + } + + private static byte[] CreateIpv6Fragment(uint id, int offset, bool moreFragments) + { + // IPv6 base header + one Hop-by-Hop header + Fragment header + UDP header. + byte[] packet = new byte[64]; + packet[0] = 0x60; + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(4, 2), 24); + packet[6] = 0; // Hop-by-Hop + packet[7] = 64; + IPAddress.Parse("2001:db8::1").GetAddressBytes().CopyTo(packet, 8); + IPAddress.Parse("2001:db8::53").GetAddressBytes().CopyTo(packet, 24); + + packet[40] = 44; // next = Fragment + packet[41] = 0; // 8-byte extension header + + packet[48] = 17; // Fragment next = UDP + ushort offsetAndFlags = checked((ushort)((offset << 3) & 0xFFF8)); + if (moreFragments) + offsetAndFlags |= 0x0001; + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(50, 2), offsetAndFlags); + BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(52, 4), id); + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(56, 2), 53000); + BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(58, 2), 53); + return packet; + } + } +} diff --git a/Shadowsocks.UnitTests/LocalizationServiceTests.cs b/Shadowsocks.UnitTests/LocalizationServiceTests.cs new file mode 100644 index 000000000..468ba3c41 --- /dev/null +++ b/Shadowsocks.UnitTests/LocalizationServiceTests.cs @@ -0,0 +1,322 @@ +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Localization; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class LocalizationServiceTests +{ + private const string Csv = "en,ru-RU,zh-CN,zh-TW,ja,ko,fr\r\n" + + "Hello,Привет,,,,,\r\n" + + "Port {0},Порт {0},,,,,\r\n"; + + [TestMethod] + public void UsesRequestedLocale() + { + var service = new CsvLocalizationService(Csv, CultureInfo.GetCultureInfo("ru-RU")); + + Assert.AreEqual("Привет", service["Hello"]); + Assert.AreEqual("Порт 1080", service.Format("Port {0}", 1080)); + } + + [TestMethod] + public void KeysAreOrdinalCaseSensitive() + { + const string caseCsv = "en,ru-RU,zh-CN,zh-TW,ja,ko,fr\r\n" + + "Active,Активен,,,,,\r\n" + + "ACTIVE,АКТИВЕН,,,,,\r\n"; + var service = new CsvLocalizationService(caseCsv, CultureInfo.GetCultureInfo("ru-RU")); + + Assert.AreEqual("Активен", service["Active"]); + Assert.AreEqual("АКТИВЕН", service["ACTIVE"]); + } + + [TestMethod] + public void EmbeddedCatalogHasNoExactDuplicateEnglishKeys() + { + var seen = new HashSet(System.StringComparer.Ordinal); + string[] duplicates = ParseCsv(Shadowsocks.Core.EmbeddedResources.I18nCsv) + .Where(row => row.Length > 0 && !string.IsNullOrWhiteSpace(row[0]) && !row[0].TrimStart().StartsWith('#')) + .Select(row => row[0].Trim()) + .Where(key => !seen.Add(key)) + .Distinct(System.StringComparer.Ordinal) + .ToArray(); + + Assert.AreEqual(0, duplicates.Length, $"Exact duplicate localization keys: {string.Join(", ", duplicates)}"); + } + + [TestMethod] + public void FallsBackToSameLanguageRegion() + { + var service = new CsvLocalizationService(Csv, CultureInfo.GetCultureInfo("ru-UA")); + + Assert.AreEqual("Привет", service["Hello"]); + } + + [TestMethod] + public void MissingTranslationFallsBackToEnglishKey() + { + var service = new CsvLocalizationService(Csv, CultureInfo.GetCultureInfo("fr-FR")); + + Assert.AreEqual("Hello", service["Hello"]); + Assert.AreEqual("Unknown", service["Unknown"]); + } + + + [TestMethod] + public void EmbeddedCatalogHasCompleteTranslations() + { + string csv = Shadowsocks.Core.EmbeddedResources.I18nCsv; + string[] locales = ["ru-RU", "zh-CN", "zh-TW", "ja", "ko", "fr"]; + string[][] rows = ParseCsv(csv) + .Where(row => row.Length >= 7 && !string.IsNullOrWhiteSpace(row[0]) && !row[0].TrimStart().StartsWith('#')) + .ToArray(); + + foreach (string[] row in rows) + { + for (int i = 0; i < locales.Length; i++) + { + Assert.IsFalse(string.IsNullOrWhiteSpace(row[i + 1]), $"Missing {locales[i]} translation for '{row[0]}'."); + } + } + } + + private static IEnumerable ParseCsv(string csv) + { + var row = new List(); + var field = new System.Text.StringBuilder(); + bool quoted = false; + for (int i = 0; i < csv.Length; i++) + { + char c = csv[i]; + if (quoted) + { + if (c == '"' && i + 1 < csv.Length && csv[i + 1] == '"') + { + field.Append('"'); + i++; + } + else if (c == '"') + { + quoted = false; + } + else + { + field.Append(c); + } + } + else if (c == '"') + { + quoted = true; + } + else if (c == ',') + { + row.Add(field.ToString()); + field.Clear(); + } + else if (c == '\r' || c == '\n') + { + if (c == '\r' && i + 1 < csv.Length && csv[i + 1] == '\n') + { + i++; + } + row.Add(field.ToString()); + field.Clear(); + if (row.Count > 1 || row.Any(value => value.Length > 0)) + { + yield return row.ToArray(); + } + row.Clear(); + } + else + { + field.Append(c); + } + } + if (field.Length > 0 || row.Count > 0) + { + row.Add(field.ToString()); + yield return row.ToArray(); + } + } + + + [TestMethod] + public void EmbeddedRussianCatalogCoversPluginManagement() + { + var service = new CsvLocalizationService(Shadowsocks.Core.EmbeddedResources.I18nCsv, CultureInfo.GetCultureInfo("ru-RU")); + string[] pluginKeys = + [ + "Plugins", "Plugin", "Add plugin", "Choose a plugin", "Install", "Import ZIP/TAR.GZ", + "Installed plugins", "No plugins installed.", "Plugin removed.", + "Install and manage SIP003 plugins.", + "Install SIP003 plugins from the built-in list or import a plugin ZIP or TAR.GZ package.", + "Optional SIP003 plugin installed on the Plugins page.", + "Choose a supported Windows x64 SIP003 plugin to install.", + "Download the latest Windows x64 release archive and install the selected plugin.", + "Import a local ZIP or TAR.GZ package containing a Windows plugin executable.", + "Remove this installed plugin from Shadowsocks storage.", + "Install SIP003 plugins from the built-in list or import a plugin ZIP or TAR.GZ package. Built-in catalog plugins can update automatically.", + "Check updates", + "Check installed catalog plugins for newer GitHub releases now.", + "Built-in catalog plugins are checked automatically once per day when they are not in use.", + "Release: {0}", "Automatic updates", + "Automatically check this catalog plugin for a newer release once per day.", + "{0} plugin(s) updated.", + "Plugin updates were deferred because one or more plugins are in use.", + "Installed catalog plugins are up to date.", + "{0} installed.", + ]; + + foreach (string key in pluginKeys) + { + Assert.AreNotEqual(key, service[key], $"Missing Russian plugin translation for '{key}'."); + } + } + + [TestMethod] + public void EmbeddedRussianCatalogCoversTrayCommands() + { + var service = new CsvLocalizationService(Shadowsocks.Core.EmbeddedResources.I18nCsv, CultureInfo.GetCultureInfo("ru-RU")); + string[] trayKeys = + [ + "System Proxy", "Disable", "PAC", "Global", "Traffic Mode", "User Mode", "Admin Mode", + "Traffic Routing", "Servers", "Share Server Config", "DNS", "System DNS", "DNSCrypt", + "DNS Settings", "Check DNSCrypt Update", "DNSCrypt: Not installed", "DNSCrypt: Starting…", "DNSCrypt: Updating…", + "Local PAC", "Online PAC", "Edit Local User Rules", + "Update Local PAC from Geosite", "GeoSite Sources", "Require secret for local PAC URL", + "Regenerate Local PAC after application updates", "Edit Online PAC URL", + "Forward Proxy", "Online Config", "Start on Boot", "Associate ss:// Links", + "Allow other devices to connect", "Hotkeys", "Help", "Logs", "Updates", + "Check for Updates", "Include prerelease versions", "About", "Quit", + "More than 20 servers (total: {0})", + ]; + + foreach (string key in trayKeys) + { + string value = service[key]; + Assert.IsFalse(string.IsNullOrWhiteSpace(value), $"Missing Russian tray translation for '{key}'."); + if (key is "DNS" or "DNSCrypt") + continue; + + Assert.AreNotEqual(key, value, $"Missing Russian tray translation for '{key}'."); + } + } + [TestMethod] + public void EmbeddedRussianCatalogCoversWinUiUxAuditStrings() + { + var service = new CsvLocalizationService(Shadowsocks.Core.EmbeddedResources.I18nCsv, CultureInfo.GetCultureInfo("ru-RU")); + string[] keys = + [ + "Open application appearance, startup and storage settings.", + "Edit Local User Rules", + "Select a GeoSite source before moving or removing it.", + "Enter an absolute HTTP or HTTPS PAC URL.", + "Select a server to edit, duplicate, share, move, or delete it.", + "Not tested", + "Registered", + "Registration failed", + "Select a server to share, copy, or delete it.", + "The selected server's ss:// URL. Use Copy to place it on the clipboard.", + "Shows the resolvers currently chosen by automatic DNSCrypt selection.", + "Release notes for the selected available update.", + "Timeout (Sec)", + "Toggle system proxy", + "Switch Global / PAC", + "Allow clients from LAN", + "Open logs window", + "Switch to previous server", + "Switch to next server", + "Address", + "Port", + "Timeout", + "Username", + "Save the online PAC URL used when Online PAC mode is enabled.", + "Download the latest Windows x64 release archive and replace the installed selected plugin.", + "Save GeoSite source changes before downloading or refreshing data.", + ]; + + foreach (string key in keys) + { + string value = service[key]; + Assert.IsFalse(string.IsNullOrWhiteSpace(value), $"Missing Russian WinUI UX translation for '{key}'."); + Assert.AreNotEqual(key, value, $"Missing Russian WinUI UX translation for '{key}'."); + } + } + + [TestMethod] + public void EmbeddedRussianCatalogCoversDnsCryptManagement() + { + var service = new CsvLocalizationService(Shadowsocks.Core.EmbeddedResources.I18nCsv, CultureInfo.GetCultureInfo("ru-RU")); + string[] keys = + [ + "DNS", "DNS status", "Active adapters", "Windows DNS servers", "Shadowsocks DNS policy", "Transparent interception", + "DNS mode", "System DNS", "Direct DNS", "DNS through Shadowsocks", "Custom DoH", "DNSCrypt", + "System DNS settings", "Direct DNS settings", "DNS through Shadowsocks settings", "Custom DoH settings", + "Original DNS destination", "DNS provider", "Primary DNS server", "Fallback DNS server", "Route Direct DNS through Shadowsocks", + "DNSCrypt Proxy", "Installed version", "Latest version", "Local port", "Coverage", "Not installed", + "Install DNSCrypt Proxy", "Check for DNSCrypt updates", "Update", "Restart", "Reinstall", "Remove", + "Automatically update DNSCrypt Proxy", "Require DNSSEC", "Require no-log resolvers", + "Require unfiltered resolvers", "Use IPv6 resolvers", "Route DNSCrypt through Shadowsocks", + "In Administrator Mode, DNSCrypt always blocks intercepted plaintext DNS while starting, restarting, or unavailable.", "Resolver selection", "Automatic", "Manual selection", + "Automatically selected resolvers", + "Available resolvers", "Load resolver list", "Search resolvers", + "Resolver list is loaded from DNSCrypt Proxy's signed upstream catalog.", + "Loading resolver list", "Resolver list loaded.", "Loaded {0} upstream resolvers.", + "Showing {0} matching resolver(s).", "Select at least one resolver.", + "DNS settings are saved and applied automatically.", "DNS settings applied.", "DNS server settings applied.", "Custom DoH settings applied.", + "Enter a valid primary IPv4 or IPv6 DNS server address.", "Enter a valid fallback IPv4 or IPv6 DNS server address.", + "Enter a primary DNS server before configuring a fallback server.", + "System DNS information is read from active Windows network adapters; Shadowsocks does not change adapter DNS addresses.", + "Windows DNS servers from active network adapters are used without modification.", + "Captured DNS queries are sent directly to their original destination and bypass Shadowsocks.", + "Captured DNS queries use the active Shadowsocks route; no separate resolver endpoint is required.", + "Direct DNS queries are routed to the selected resolver through Shadowsocks.", + "DNS queries are sent as DNS-over-HTTPS to the configured endpoint.", + "Use DNS servers configured by Windows without Shadowsocks DNS redirection.", + "Send captured DNS traffic directly to its original destination. Transparent enforcement requires Administrator Mode.", + "Route captured DNS traffic through the active Shadowsocks server. Transparent enforcement requires Administrator Mode.", + "Convert captured DNS queries to DNS-over-HTTPS using the configured HTTPS endpoint.", + "Use DNSCrypt Proxy for Shadowsocks-managed DNS and transparent DNS capture when available.", + "Enter an absolute HTTPS DNS-over-HTTPS endpoint. Changes are applied automatically when you leave the field.", + "Protocol", + "All protocols", + "Automatically select a resolver from DNSCrypt Proxy's signed catalog using the DNSSEC, no-log, unfiltered and address-family settings.", + "Choose one or more DNSCrypt or DoH resolvers manually from the signed upstream catalog.", + "Automatic mode selects a filtered resolver from DNSCrypt Proxy's signed catalog.", + "Automatic filtered selection is active ({0} resolver(s)).", + "Automatic filtered resolver is starting.", + "Automatic mode selects a resolver from the signed catalog using the DNSSEC, no-log, unfiltered and address-family settings above.", + "Filter the signed resolver catalog by transport protocol: DNSCrypt or DNS-over-HTTPS.", + "Select the DNSCrypt or DoH resolvers DNSCrypt Proxy may use in manual-selection mode. Ping is measured asynchronously; the active resolver is replaced with DNSCrypt Proxy's actual RTT when available.", + "Choose one or more DNSCrypt resolvers manually from the signed upstream catalog.", + "Filter resolvers by name, protocol, country code, country name, or description.", + "Select the resolvers DNSCrypt Proxy may use in manual-selection mode.", + "Checking latest version", + "Downloading DNSCrypt Proxy", "Verifying signature", "Validating DNSCrypt runtime", + "DNSCrypt Proxy is not installed.", "Download and enable", "DNSCrypt operation failed. See Logs for details.", + "DNS operation failed. See Logs for details.", "Enter a valid HTTPS DoH URL to enable Custom DoH.", + "Administrator Mode transparently redirects captured UDP/TCP DNS traffic to DNSCrypt. User and Game modes do not provide system-wide interception.", + "Transparent DNS interception is active for captured applications.", + "DNS is blocked (fail-closed) because DNSCrypt is unavailable.", + "DNSCrypt is running, but system-wide interception is paused in Game Mode.", + "DNSCrypt is running; full DNS interception requires Administrator Mode.", + "System-wide DNS interception is paused in Game Mode.", + "DNSCrypt is unavailable; full DNS interception requires Administrator Mode.", + ]; + + foreach (string key in keys) + { + string value = service[key]; + Assert.IsFalse(string.IsNullOrWhiteSpace(value), $"Missing Russian DNSCrypt translation for '{key}'."); + if (key is "DNS" or "DNSCrypt" or "DNSCrypt Proxy") + continue; + + Assert.AreNotEqual(key, value, $"Missing Russian DNSCrypt translation for '{key}'."); + } + } + +} diff --git a/Shadowsocks.UnitTests/ManagedRoutingArchitectureTests.cs b/Shadowsocks.UnitTests/ManagedRoutingArchitectureTests.cs new file mode 100644 index 000000000..25d8c468b --- /dev/null +++ b/Shadowsocks.UnitTests/ManagedRoutingArchitectureTests.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class ManagedRoutingArchitectureTests +{ + [TestMethod] + public void LocalPacIsOnlyMinimalManagedProxyFunnel() + { + string pac = PACDaemon.GetManagedFunnelPac(); + + Assert.AreEqual("function FindProxyForURL(url, host) { return __PROXY__; }\n", pac); + Assert.IsFalse(pac.Contains("__RULES__", StringComparison.Ordinal)); + Assert.IsFalse(pac.Contains("RegExp", StringComparison.Ordinal)); + Assert.IsFalse(pac.Contains("CombinedMatcher", StringComparison.Ordinal)); + Assert.IsFalse(pac.Contains("RegExpFilter", StringComparison.Ordinal)); + } + + [TestMethod] + public void LocalManagedSnapshotAlwaysUsesCSharpAuthority() + { + var configuration = new Configuration + { + Enabled = true, + global = false, + useOnlinePac = false, + }; + + var snapshot = ManagedRoutingSnapshotBuilder.Build(configuration); + + Assert.IsTrue(snapshot.IsEnabled); + Assert.IsFalse(snapshot.Source.Contains("compatibility", StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(snapshot.Source.Contains("JavaScript", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/Shadowsocks.UnitTests/ManagedRoutingIntegrationTests.cs b/Shadowsocks.UnitTests/ManagedRoutingIntegrationTests.cs new file mode 100644 index 000000000..12f532b12 --- /dev/null +++ b/Shadowsocks.UnitTests/ManagedRoutingIntegrationTests.cs @@ -0,0 +1,218 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; +using Shadowsocks.NetworkService.Ipc; +using Shadowsocks.NetworkService.Routing; + +namespace Shadowsocks.UnitTests; + +[TestClass] +public class ManagedRoutingIntegrationTests +{ + [TestMethod] + public void LocalPacManagedFunnelContainsNoEmbeddedRuleRuntime() + { + string pac = PACDaemon.GetManagedFunnelPac(); + + StringAssert.Contains(pac, "FindProxyForURL"); + StringAssert.Contains(pac, "__PROXY__"); + Assert.IsFalse(pac.Contains("__RULES__", StringComparison.Ordinal)); + Assert.IsFalse(pac.Contains("RegExpFilter", StringComparison.Ordinal)); + Assert.IsFalse(pac.Contains("CombinedMatcher", StringComparison.Ordinal)); + } + + [TestMethod] + public void ManagedAdminPolicyDefersOnlyUnmatchedOrdinaryTcp() + { + StartRequest request = new() + { + MainProcessId = 111, + DefaultRoute = RouteAction.Direct, + ManagedRouting = new ManagedRoutingDto + { + Enabled = true, + DefaultRules = ["||proxy.example^"], + }, + ApplicationRules = + [ + new ApplicationRuleDto + { + Enabled = true, + Application = "forced.exe", + Action = RouteAction.Proxy, + }, + new ApplicationRuleDto + { + Enabled = true, + Application = "defaulted.exe", + Action = RouteAction.Default, + }, + ], + }; + ApplicationPolicy policy = new(request); + + Assert.AreEqual(RouteAction.Deferred, policy.Evaluate(6, 443, 222, null, "browser")); + Assert.AreEqual(RouteAction.Direct, policy.Evaluate(17, 443, 222, null, "browser")); + Assert.AreEqual(RouteAction.Direct, policy.Evaluate(6, 53, 222, null, "browser")); + Assert.AreEqual(RouteAction.Proxy, policy.Evaluate(6, 443, 222, null, "forced")); + Assert.AreEqual(RouteAction.Deferred, policy.Evaluate(6, 443, 222, null, "defaulted")); + } + + [TestMethod] + public void NetworkServiceLinkedFilterEngineUsesSameAbpSemantics() + { + Shadowsocks.NetworkService.ManagedRouting.FilterEngine engine = + Shadowsocks.NetworkService.ManagedRouting.FilterEngine.Compile( + new[] { "||proxy.example^", "@@||direct.example^" }, + Array.Empty(), + out _); + + Assert.AreEqual( + Shadowsocks.NetworkService.ManagedRouting.FilterRoutingAction.Proxy, + engine.Evaluate("https://proxy.example/", "proxy.example").Action); + Assert.AreEqual( + Shadowsocks.NetworkService.ManagedRouting.FilterRoutingAction.Direct, + engine.Evaluate("https://direct.example/", "direct.example").Action); + } + + [TestMethod] + public void NetworkServiceStartRequestRoundTripsManagedRuleSet() + { + StartRequest request = new() + { + ManagedRouting = new ManagedRoutingDto + { + Enabled = true, + Source = "test snapshot", + DefaultRules = ["||proxy.example^", "@@||direct.example^"], + UserRules = ["||user.example^"], + }, + }; + + string json = JsonSerializer.Serialize(request, NetworkServiceJsonContext.Default.StartRequest); + StartRequest parsed = JsonSerializer.Deserialize(json, NetworkServiceJsonContext.Default.StartRequest); + + Assert.IsNotNull(parsed); + Assert.IsTrue(parsed.ManagedRouting.Enabled); + Assert.AreEqual("test snapshot", parsed.ManagedRouting.Source); + CollectionAssert.AreEqual( + new[] { "||proxy.example^", "@@||direct.example^" }, + parsed.ManagedRouting.DefaultRules); + CollectionAssert.AreEqual(new[] { "||user.example^" }, parsed.ManagedRouting.UserRules); + } + + [TestMethod] + public void HttpHostInspectionBuildsRoutingUrlWithPath() + { + byte[] request = Encoding.ASCII.GetBytes( + "GET /ads/banner.js?x=1 HTTP/1.1\r\nHost: cdn.example.com\r\nConnection: close\r\n\r\n"); + + TcpDestinationInspectionStatus status = InitialTcpProtocolInspector.Inspect( + request, + 80, + out TcpDestinationIdentity identity); + + Assert.AreEqual(TcpDestinationInspectionStatus.Identified, status); + Assert.AreEqual("cdn.example.com", identity.Host); + Assert.AreEqual("http://cdn.example.com/ads/banner.js?x=1", identity.Url); + Assert.AreEqual("http", identity.Protocol); + } + + [TestMethod] + public void PartialHttpHeadersRequestMoreData() + { + byte[] request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost: partial.example"); + + TcpDestinationInspectionStatus status = InitialTcpProtocolInspector.Inspect( + request, + 80, + out _); + + Assert.AreEqual(TcpDestinationInspectionStatus.NeedMoreData, status); + } + + [TestMethod] + public void TlsClientHelloSniIsExtractedWithoutDecryption() + { + byte[] hello = BuildClientHello("secure.example.com"); + + TcpDestinationInspectionStatus status = InitialTcpProtocolInspector.Inspect( + hello, + 443, + out TcpDestinationIdentity identity); + + Assert.AreEqual(TcpDestinationInspectionStatus.Identified, status); + Assert.AreEqual("secure.example.com", identity.Host); + Assert.AreEqual("https://secure.example.com/", identity.Url); + Assert.AreEqual("tls-sni", identity.Protocol); + } + + [TestMethod] + public void PartialTlsClientHelloRequestsMoreData() + { + byte[] hello = BuildClientHello("secure.example.com"); + byte[] partial = hello[..Math.Min(12, hello.Length)]; + + TcpDestinationInspectionStatus status = InitialTcpProtocolInspector.Inspect( + partial, + 443, + out _); + + Assert.AreEqual(TcpDestinationInspectionStatus.NeedMoreData, status); + } + + private static byte[] BuildClientHello(string host) + { + byte[] hostBytes = Encoding.ASCII.GetBytes(host); + using var extensions = new System.IO.MemoryStream(); + WriteUInt16(extensions, 0x0000); + int serverNameDataLength = 2 + 1 + 2 + hostBytes.Length; + WriteUInt16(extensions, serverNameDataLength); + WriteUInt16(extensions, 1 + 2 + hostBytes.Length); + extensions.WriteByte(0); + WriteUInt16(extensions, hostBytes.Length); + extensions.Write(hostBytes, 0, hostBytes.Length); + + using var body = new System.IO.MemoryStream(); + body.WriteByte(0x03); + body.WriteByte(0x03); + body.Write(new byte[32], 0, 32); + body.WriteByte(0); // session id + WriteUInt16(body, 2); + body.WriteByte(0x13); + body.WriteByte(0x01); + body.WriteByte(1); // compression methods length + body.WriteByte(0); + byte[] extensionBytes = extensions.ToArray(); + WriteUInt16(body, extensionBytes.Length); + body.Write(extensionBytes, 0, extensionBytes.Length); + + byte[] helloBody = body.ToArray(); + using var handshake = new System.IO.MemoryStream(); + handshake.WriteByte(0x01); + handshake.WriteByte((byte)((helloBody.Length >> 16) & 0xff)); + handshake.WriteByte((byte)((helloBody.Length >> 8) & 0xff)); + handshake.WriteByte((byte)(helloBody.Length & 0xff)); + handshake.Write(helloBody, 0, helloBody.Length); + byte[] handshakeBytes = handshake.ToArray(); + + using var record = new System.IO.MemoryStream(); + record.WriteByte(0x16); + record.WriteByte(0x03); + record.WriteByte(0x01); + WriteUInt16(record, handshakeBytes.Length); + record.Write(handshakeBytes, 0, handshakeBytes.Length); + return record.ToArray(); + } + + private static void WriteUInt16(System.IO.MemoryStream stream, int value) + { + Span bytes = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(bytes, checked((ushort)value)); + stream.Write(bytes); + } +} diff --git a/Shadowsocks.UnitTests/PluginManagerTests.cs b/Shadowsocks.UnitTests/PluginManagerTests.cs new file mode 100644 index 000000000..838c951c5 --- /dev/null +++ b/Shadowsocks.UnitTests/PluginManagerTests.cs @@ -0,0 +1,525 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Formats.Tar; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + [DoNotParallelize] + public class PluginManagerTests + { + private string _root; + private string _pluginsRoot; + + [TestInitialize] + public void Initialize() + { + _root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", Guid.NewGuid().ToString("N")); + _pluginsRoot = Path.Combine(_root, "Plugins"); + Directory.CreateDirectory(_root); + PluginManager.PluginsDirectoryOverride = _pluginsRoot; + } + + [TestCleanup] + public void Cleanup() + { + PluginManager.PluginsDirectoryOverride = null; + PluginManager.HttpClientOverride = null; + try { Directory.Delete(_root, recursive: true); } catch { } + } + + [TestMethod] + public void CatalogContainsSupportedWindowsPlugins() + { + CollectionAssert.AreEquivalent( + new[] { "xray-plugin", "v2ray-plugin", "qtun" }, + PluginManager.Catalog.Select(entry => entry.Id).ToArray()); + } + + [TestMethod] + public async Task QtunCatalogMatchesUpstreamWindowsReleaseArchive() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "qtun"); + Assert.AreEqual("x86_64-pc-windows-msvc", entry.WindowsAssetMarker); + + using var client = new HttpClient(new QtunReleaseHandler()); + PluginManager.HttpClientOverride = client; + + InstalledPlugin installed = await PluginManager.InstallCatalogPluginAsync(entry); + + Assert.AreEqual("qtun", installed.Id); + Assert.AreEqual("v0.2.0", installed.ReleaseTag); + Assert.AreEqual("shadowsocks-v0.2.0.x86_64-pc-windows-msvc.zip", installed.AssetName); + Assert.AreEqual("qtun-client.exe", Path.GetFileName(installed.ExecutablePath)); + } + + [TestMethod] + public void CatalogLookupRecognizesShareLinkPluginExecutableNames() + { + Assert.AreEqual("xray-plugin", PluginManager.FindCatalogEntry("xray-plugin.exe")?.Id); + Assert.AreEqual("v2ray-plugin", PluginManager.FindCatalogEntry("v2ray-plugin.exe")?.Id); + Assert.AreEqual("qtun", PluginManager.FindCatalogEntry("qtun-client.exe")?.Id); + Assert.IsNull(PluginManager.FindCatalogEntry("unknown-plugin.exe")); + } + + [TestMethod] + public void ManualZipInstallsUnderPluginStorageAndResolvesExecutable() + { + string zip = CreateZip("custom-plugin.zip", + ("custom-plugin.exe", new byte[] { 0x4D, 0x5A }), + ("data/config.json", new byte[] { 0x7B, 0x7D })); + + InstalledPlugin installed = PluginManager.InstallManualZip(zip); + + Assert.AreEqual("custom-plugin", installed.Id); + Assert.AreEqual("Manual ZIP", installed.Source); + Assert.IsFalse(installed.CanAutoUpdate); + Assert.IsFalse(installed.AutoUpdate); + Assert.IsTrue(File.Exists(installed.ExecutablePath)); + Assert.IsTrue(installed.ExecutablePath.StartsWith(Path.GetFullPath(_pluginsRoot), StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual(installed.ExecutablePath, PluginManager.ResolveExecutable("custom-plugin")); + Assert.AreEqual(installed.ExecutablePath, PluginManager.ResolveExecutable("custom-plugin.exe")); + Assert.AreEqual(1, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ManualTarGzInstallsUnderPluginStorageAndResolvesExecutable() + { + string archive = CreateTarGz("custom-tar-plugin.tar.gz", + ("custom-tar-plugin.exe", new byte[] { 0x4D, 0x5A }), + ("data/config.json", new byte[] { 0x7B, 0x7D })); + + InstalledPlugin installed = PluginManager.InstallManualArchive(archive); + + Assert.AreEqual("custom-tar-plugin", installed.Id); + Assert.AreEqual("Manual TAR.GZ", installed.Source); + Assert.IsFalse(installed.CanAutoUpdate); + Assert.IsFalse(installed.AutoUpdate); + Assert.IsTrue(File.Exists(installed.ExecutablePath)); + Assert.IsTrue(installed.ExecutablePath.StartsWith(Path.GetFullPath(_pluginsRoot), StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual(installed.ExecutablePath, PluginManager.ResolveExecutable("custom-tar-plugin")); + Assert.AreEqual(1, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ManualZipSelectsSingleClientExecutableWhenPackageContainsClientAndServer() + { + string zip = CreateZip("qtun.zip", + ("bin/qtun-client.exe", new byte[] { 0x4D, 0x5A }), + ("bin/qtun-server.exe", new byte[] { 0x4D, 0x5A })); + + InstalledPlugin installed = PluginManager.InstallManualZip(zip); + + Assert.AreEqual("qtun", installed.Id); + Assert.AreEqual("qtun-client.exe", Path.GetFileName(installed.ExecutablePath)); + } + + [TestMethod] + public void ManualImportRejectsUnsupportedArchiveFiles() + { + Assert.ThrowsExactly(() => PluginManager.InstallManualArchive(Path.Combine(_root, "plugin.exe"))); + } + + [TestMethod] + public void ManualImportRejectsZipWithoutExecutable() + { + string zip = CreateZip("no-exe.zip", ("README.txt", new byte[] { 1, 2, 3 })); + + Assert.ThrowsExactly(() => PluginManager.InstallManualZip(zip)); + Assert.AreEqual(0, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ManualImportRejectsZipPathTraversal() + { + string zip = CreateZip("unsafe.zip", ("../escaped.exe", new byte[] { 0x4D, 0x5A })); + + Assert.ThrowsExactly(() => PluginManager.InstallManualZip(zip)); + Assert.IsFalse(File.Exists(Path.Combine(_pluginsRoot, "escaped.exe"))); + Assert.AreEqual(0, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ManualImportRejectsWindowsAlternateDataStreamPath() + { + string zip = CreateZip("unsafe-ads.zip", ("plugin.exe:payload", new byte[] { 0x4D, 0x5A })); + + Assert.ThrowsExactly(() => PluginManager.InstallManualZip(zip)); + Assert.AreEqual(0, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ManualImportRejectsTarGzPathTraversal() + { + string archive = CreateTarGz("unsafe.tar.gz", ("../escaped.exe", new byte[] { 0x4D, 0x5A })); + + Assert.ThrowsExactly(() => PluginManager.InstallManualArchive(archive)); + Assert.IsFalse(File.Exists(Path.Combine(_pluginsRoot, "escaped.exe"))); + Assert.AreEqual(0, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public void ExtractionBudgetTracksActualBytesAndRejectsOverflow() + { + const long Limit = 512L * 1024 * 1024; + + Assert.AreEqual(Limit, PluginManager.AccumulateExtractedBytes(Limit - 1, 1, "ZIP")); + Assert.ThrowsExactly(() => + PluginManager.AccumulateExtractedBytes(Limit - 1, 2, "ZIP")); + } + + [TestMethod] + public async Task CatalogPluginStoresReleaseMetadataAndAutoUpdates() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var firstClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = firstClient; + + InstalledPlugin installed = await PluginManager.InstallCatalogPluginAsync(entry); + + Assert.IsTrue(installed.CanAutoUpdate); + Assert.IsTrue(installed.AutoUpdate); + Assert.AreEqual(entry.Repository, installed.Repository); + Assert.AreEqual("v1.0.0", installed.ReleaseTag); + CollectionAssert.AreEqual(new byte[] { 0x4D, 0x5A, 0x01 }, File.ReadAllBytes(installed.ExecutablePath)); + + using var secondClient = new HttpClient(new FakeReleaseHandler("v1.1.0", 0x02)); + PluginManager.HttpClientOverride = secondClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + + Assert.AreEqual(1, summary.CheckedCount); + Assert.AreEqual(1, summary.UpdatedCount); + Assert.AreEqual(0, summary.Errors.Count); + InstalledPlugin updated = PluginManager.GetInstalledPlugins().Single(); + Assert.AreEqual("v1.1.0", updated.ReleaseTag); + CollectionAssert.AreEqual(new byte[] { 0x4D, 0x5A, 0x02 }, File.ReadAllBytes(updated.ExecutablePath)); + } + + [TestMethod] + public async Task CatalogPluginRejectsMismatchedGitHubDigest() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var client = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01, useInvalidDigest: true)); + PluginManager.HttpClientOverride = client; + + await Assert.ThrowsExactlyAsync(() => PluginManager.InstallCatalogPluginAsync(entry)); + + Assert.AreEqual(0, PluginManager.GetInstalledPlugins().Count); + } + + [TestMethod] + public async Task FailedAutomaticCheckDoesNotPostponeRetryWindow() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var installClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = installClient; + await PluginManager.InstallCatalogPluginAsync(entry); + DateTimeOffset? lastSuccessfulCheck = PluginManager.GetInstalledPlugins().Single().LastUpdateCheckUtc; + + using var failingClient = new HttpClient(new StatusCodeHandler(HttpStatusCode.ServiceUnavailable)); + PluginManager.HttpClientOverride = failingClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + + Assert.AreEqual(1, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + Assert.AreEqual(1, summary.Errors.Count); + Assert.AreEqual(lastSuccessfulCheck, PluginManager.GetInstalledPlugins().Single().LastUpdateCheckUtc); + } + + [TestMethod] + public async Task SuccessfulNoOpCheckAdvancesRetryWindow() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var installClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = installClient; + await PluginManager.InstallCatalogPluginAsync(entry); + DateTimeOffset? before = PluginManager.GetInstalledPlugins().Single().LastUpdateCheckUtc; + + using var checkClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = checkClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + DateTimeOffset? after = PluginManager.GetInstalledPlugins().Single().LastUpdateCheckUtc; + + Assert.AreEqual(1, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + Assert.AreEqual(0, summary.Errors.Count); + Assert.IsTrue(after >= before); + } + + [TestMethod] + public async Task CatalogAutoUpdateRejectsVersionDowngrade() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var installClient = new HttpClient(new FakeReleaseHandler("v1.1.0", 0x02)); + PluginManager.HttpClientOverride = installClient; + InstalledPlugin installed = await PluginManager.InstallCatalogPluginAsync(entry); + DateTimeOffset? before = installed.LastUpdateCheckUtc; + + using var downgradeClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = downgradeClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + InstalledPlugin after = PluginManager.GetInstalledPlugins().Single(); + + Assert.AreEqual(1, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + Assert.AreEqual(0, summary.Errors.Count); + Assert.AreEqual("v1.1.0", after.ReleaseTag); + Assert.IsTrue(after.LastUpdateCheckUtc >= before); + CollectionAssert.AreEqual(new byte[] { 0x4D, 0x5A, 0x02 }, File.ReadAllBytes(after.ExecutablePath)); + } + + [TestMethod] + public async Task DisabledCatalogAutoUpdateDoesNotCallGitHub() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var installClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = installClient; + await PluginManager.InstallCatalogPluginAsync(entry); + Assert.IsTrue(PluginManager.SetAutoUpdate(entry.Id, false)); + + using var throwingClient = new HttpClient(new ThrowingHandler()); + PluginManager.HttpClientOverride = throwingClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + + Assert.AreEqual(0, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + Assert.IsFalse(PluginManager.GetInstalledPlugins().Single().AutoUpdate); + } + + [TestMethod] + public async Task ManualPluginWithCatalogIdIsNotAutoUpdated() + { + string zip = CreateZip("xray-plugin.zip", ("xray-plugin.exe", new byte[] { 0x4D, 0x5A })); + InstalledPlugin installed = PluginManager.InstallManualZip(zip); + Assert.AreEqual("xray-plugin", installed.Id); + Assert.IsFalse(installed.CanAutoUpdate); + + using var throwingClient = new HttpClient(new ThrowingHandler()); + PluginManager.HttpClientOverride = throwingClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(force: true); + + Assert.AreEqual(0, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + } + + [TestMethod] + public async Task CatalogPluginInUseIsDeferredWithoutNetworkAccess() + { + PluginCatalogEntry entry = PluginManager.Catalog.Single(candidate => candidate.Id == "xray-plugin"); + using var installClient = new HttpClient(new FakeReleaseHandler("v1.0.0", 0x01)); + PluginManager.HttpClientOverride = installClient; + await PluginManager.InstallCatalogPluginAsync(entry); + + using var throwingClient = new HttpClient(new ThrowingHandler()); + PluginManager.HttpClientOverride = throwingClient; + PluginUpdateSummary summary = await PluginManager.UpdateCatalogPluginsAsync(new[] { entry.Id }, force: true); + + Assert.AreEqual(0, summary.CheckedCount); + Assert.AreEqual(0, summary.UpdatedCount); + Assert.AreEqual(1, summary.SkippedInUseCount); + } + + [TestMethod] + public void AutomaticCheckPolicyUsesTwentyFourHourIntervalAndRecoversFromClockRollback() + { + DateTimeOffset now = new(2026, 8, 22, 10, 0, 0, TimeSpan.Zero); + + Assert.IsTrue(PluginManager.IsAutomaticCheckDue(null, now)); + Assert.IsFalse(PluginManager.IsAutomaticCheckDue(now.AddHours(-23), now)); + Assert.IsTrue(PluginManager.IsAutomaticCheckDue(now.AddHours(-24), now)); + Assert.IsTrue(PluginManager.IsAutomaticCheckDue(now.AddMinutes(1), now)); + } + + [TestMethod] + public void InterruptedSwapRestoresOrphanedPluginBackupOnNextStorageRead() + { + string zip = CreateZip("recovery-plugin.zip", ("recovery-plugin.exe", new byte[] { 0x4D, 0x5A })); + InstalledPlugin installed = PluginManager.InstallManualZip(zip); + string destination = Path.Combine(_pluginsRoot, installed.Id); + string backup = Path.Combine(_pluginsRoot, $".plugin-backup-{installed.Id}-{Guid.NewGuid():N}"); + Directory.Move(destination, backup); + + Assert.IsFalse(Directory.Exists(destination)); + InstalledPlugin recovered = PluginManager.GetInstalledPlugins().Single(); + + Assert.AreEqual(installed.Id, recovered.Id); + Assert.IsTrue(Directory.Exists(destination)); + Assert.IsFalse(Directory.Exists(backup)); + Assert.IsTrue(File.Exists(recovered.ExecutablePath)); + } + + [TestMethod] + public void RemoveDeletesInstalledPlugin() + { + string zip = CreateZip("remove-me.zip", ("remove-me.exe", new byte[] { 0x4D, 0x5A })); + InstalledPlugin installed = PluginManager.InstallManualZip(zip); + + PluginManager.Remove(installed.Id); + + Assert.IsFalse(Directory.Exists(Path.Combine(_pluginsRoot, installed.Id))); + Assert.IsNull(PluginManager.ResolveExecutable(installed.Id)); + } + + private sealed class FakeReleaseHandler : HttpMessageHandler + { + private readonly string _tag; + private readonly byte[] _payload; + private readonly bool _useInvalidDigest; + + public FakeReleaseHandler(string tag, byte payloadVersion, bool useInvalidDigest = false) + { + _tag = tag; + _payload = CreateZipBytes(payloadVersion); + _useInvalidDigest = useInvalidDigest; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri?.Host.Equals("api.github.com", StringComparison.OrdinalIgnoreCase) == true) + { + string digest = _useInvalidDigest + ? new string('0', 64) + : Convert.ToHexString(SHA256.HashData(_payload)).ToLowerInvariant(); + string json = "{\"tag_name\":\"" + _tag + "\",\"assets\":[{" + + "\"name\":\"xray-plugin-windows-amd64.zip\"," + + "\"browser_download_url\":\"https://downloads.test/xray-plugin.zip\"," + + "\"digest\":\"sha256:" + digest + "\"}]}"; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }); + } + + if (request.RequestUri?.Host.Equals("downloads.test", StringComparison.OrdinalIgnoreCase) == true) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_payload), + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + + private static byte[] CreateZipBytes(byte payloadVersion) + { + using var stream = new MemoryStream(); + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) + { + ZipArchiveEntry entry = archive.CreateEntry("xray-plugin.exe"); + using Stream output = entry.Open(); + output.Write(new byte[] { 0x4D, 0x5A, payloadVersion }); + } + return stream.ToArray(); + } + } + + private sealed class QtunReleaseHandler : HttpMessageHandler + { + private readonly byte[] _payload = CreateQtunZipBytes(); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri?.Host.Equals("api.github.com", StringComparison.OrdinalIgnoreCase) == true) + { + string digest = Convert.ToHexString(SHA256.HashData(_payload)).ToLowerInvariant(); + const string assetName = "shadowsocks-v0.2.0.x86_64-pc-windows-msvc.zip"; + string json = "{\"tag_name\":\"v0.2.0\",\"assets\":[{" + + "\"name\":\"" + assetName + "\"," + + "\"browser_download_url\":\"https://downloads.test/qtun.zip\"," + + "\"digest\":\"sha256:" + digest + "\"}]}"; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }); + } + + if (request.RequestUri?.Host.Equals("downloads.test", StringComparison.OrdinalIgnoreCase) == true) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_payload), + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + + private static byte[] CreateQtunZipBytes() + { + using var stream = new MemoryStream(); + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach (string fileName in new[] { "qtun-client.exe", "qtun-server.exe" }) + { + ZipArchiveEntry entry = archive.CreateEntry(fileName); + using Stream output = entry.Open(); + output.Write(new byte[] { 0x4D, 0x5A, 0x20 }); + } + } + return stream.ToArray(); + } + } + + private sealed class StatusCodeHandler : HttpMessageHandler + { + private readonly HttpStatusCode _statusCode; + + public StatusCodeHandler(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => Task.FromResult(new HttpResponseMessage(_statusCode)); + } + + private sealed class ThrowingHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => throw new AssertFailedException($"Unexpected network request: {request.RequestUri}"); + } + + private string CreateTarGz(string name, params (string Name, byte[] Content)[] entries) + { + string path = Path.Combine(_root, name); + using FileStream stream = new(path, FileMode.Create, FileAccess.Write, FileShare.None); + using var gzip = new GZipStream(stream, CompressionLevel.SmallestSize); + using var writer = new TarWriter(gzip, leaveOpen: false); + foreach ((string entryName, byte[] content) in entries) + { + using var data = new MemoryStream(content, writable: false); + var entry = new PaxTarEntry(TarEntryType.RegularFile, entryName) + { + DataStream = data, + }; + writer.WriteEntry(entry); + } + return path; + } + + private string CreateZip(string name, params (string Name, byte[] Content)[] entries) + { + string path = Path.Combine(_root, name); + using FileStream stream = new(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + using var archive = new ZipArchive(stream, ZipArchiveMode.Create); + foreach ((string entryName, byte[] content) in entries) + { + ZipArchiveEntry entry = archive.CreateEntry(entryName); + using Stream target = entry.Open(); + target.Write(content, 0, content.Length); + } + return path; + } + } +} diff --git a/Shadowsocks.UnitTests/ProcessAttributionCacheTests.cs b/Shadowsocks.UnitTests/ProcessAttributionCacheTests.cs new file mode 100644 index 000000000..cddf1ebf7 --- /dev/null +++ b/Shadowsocks.UnitTests/ProcessAttributionCacheTests.cs @@ -0,0 +1,82 @@ +#nullable enable +using System; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.NetworkService.Routing; +using Shadowsocks.NetworkService.WinDivert; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class ProcessAttributionCacheTests + { + private static readonly DateTime Now = new(2026, 8, 18, 12, 0, 0, DateTimeKind.Utc); + + [TestInitialize] + public void Initialize() + { + ProcessAttributionCache.ClearForTests(); + } + + [TestMethod] + public void FlowObservationResolvesHostAndNetworkOrderPortForms() + { + const ushort port = 53000; + ProcessAttributionCache.Observe(17, port, 4242, Now); + + Assert.IsTrue(ProcessAttributionCache.TryGetProcessId(17, port, Now, out int direct)); + Assert.AreEqual(4242, direct); + + ushort reversed = BinaryPrimitives.ReverseEndianness(port); + Assert.IsTrue(ProcessAttributionCache.TryGetProcessId(17, reversed, Now, out int reverse)); + Assert.AreEqual(4242, reverse); + } + + [TestMethod] + public void AmbiguousSharedPortFallsBackInsteadOfGuessingPid() + { + ProcessAttributionCache.Observe(17, 54000, 100, Now); + ProcessAttributionCache.Observe(17, 54000, 200, Now); + + Assert.IsFalse(ProcessAttributionCache.TryGetProcessId(17, 54000, Now, out int processId)); + Assert.AreEqual(0, processId); + } + + [TestMethod] + public void FlowDeletionRemovesOnlyMatchingPid() + { + ProcessAttributionCache.Observe(6, 55000, 100, Now); + ProcessAttributionCache.Observe(6, 55000, 200, Now); + ProcessAttributionCache.Remove(6, 55000, 100); + + Assert.IsTrue(ProcessAttributionCache.TryGetProcessId(6, 55000, Now, out int processId)); + Assert.AreEqual(200, processId); + } + + [TestMethod] + public void StaleFlowOwnershipExpires() + { + ProcessAttributionCache.Observe(17, 56000, 4242, Now); + + Assert.IsFalse(ProcessAttributionCache.TryGetProcessId( + 17, + 56000, + Now.AddMinutes(6), + out _)); + } + + [TestMethod] + public void NativeFlowLayoutFitsWinDivertAddressUnionAndEventBitsDecode() + { + Assert.AreEqual(64, Marshal.SizeOf()); + Assert.AreEqual(80, Marshal.SizeOf()); + + WinDivertAddress address = new() + { + BitFields = (uint)WinDivertEvent.FlowEstablished << 8, + }; + Assert.AreEqual(WinDivertEvent.FlowEstablished, address.Event); + } + } +} diff --git a/test/ProcessEnvironment.cs b/Shadowsocks.UnitTests/ProcessEnvironment.cs similarity index 92% rename from test/ProcessEnvironment.cs rename to Shadowsocks.UnitTests/ProcessEnvironment.cs index 106afee2d..642616e2a 100644 --- a/test/ProcessEnvironment.cs +++ b/Shadowsocks.UnitTests/ProcessEnvironment.cs @@ -6,7 +6,7 @@ running in a Windows system. History: - v1.2.ss Add GetCommandLine for convenience. - + - v1.2: Added support for inspection of 64 bit processes from 32 bit host - v1.1: Fixed issue with environment block size detection - v1.0: Initial @@ -42,13 +42,13 @@ THE SOFTWARE. using System; using System.Collections.Specialized; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Management; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -namespace Shadowsocks.Test +namespace Shadowsocks.UnitTests { static class ProcessEnvironment { @@ -111,7 +111,7 @@ public static implicit operator UniPtr(IntPtr p) public override string ToString() { - return Value.ToString(); + return Value.ToString(CultureInfo.InvariantCulture); } public bool FitsInNativePointer @@ -155,7 +155,7 @@ static StringDictionary _GetEnvironmentVariablesCore(IntPtr hProcess) { int dataSize; if (!_HasReadAccess(hProcess, penv, out dataSize)) - throw new Exception("Unable to read environment block."); + throw new InvalidOperationException("Unable to read environment block."); if (dataSize > maxEnvSize) dataSize = maxEnvSize; @@ -169,8 +169,8 @@ static StringDictionary _GetEnvironmentVariablesCore(IntPtr hProcess) new IntPtr(dataSize), ref res_len); - if (!b || (int)res_len != dataSize) - throw new Exception("Unable to read environment block data."); + if (!b || res_len.ToInt64() != dataSize) + throw new InvalidOperationException("Unable to read environment block data."); } else if (penv.Size == 8 && IntPtr.Size == 4) { @@ -178,7 +178,7 @@ static StringDictionary _GetEnvironmentVariablesCore(IntPtr hProcess) int dataSize; if (!_HasReadAccessWow64(hProcess, penv.ToInt64(), out dataSize)) - throw new Exception("Unable to read environment block with WOW64 API."); + throw new InvalidOperationException("Unable to read environment block with WOW64 API."); if (dataSize > maxEnvSize) dataSize = maxEnvSize; @@ -193,11 +193,11 @@ static StringDictionary _GetEnvironmentVariablesCore(IntPtr hProcess) ref res_len); if (result != WindowsApi.STATUS_SUCCESS || res_len != dataSize) - throw new Exception("Unable to read environment block data with WOW64 API."); + throw new InvalidOperationException("Unable to read environment block data with WOW64 API."); } else { - throw new Exception("Unable to access process memory due to unsupported bitness cardinality."); + throw new InvalidOperationException("Unable to access process memory due to unsupported bitness cardinality."); } return _EnvToDictionary(envData); @@ -265,13 +265,12 @@ static StringDictionary _EnvToDictionary(byte[] env) static bool _TryReadIntPtr32(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) { bool result; - RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { - int dataSize = sizeof(Int32); + int dataSize = sizeof(int); var data = Marshal.AllocHGlobal(dataSize); IntPtr res_len = IntPtr.Zero; bool b = WindowsApi.ReadProcessMemory( @@ -282,7 +281,7 @@ static bool _TryReadIntPtr32(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) ref res_len); readPtr = new IntPtr(Marshal.ReadInt32(data)); Marshal.FreeHGlobal(data); - if (!b || (int)res_len != dataSize) + if (!b || res_len.ToInt64() != dataSize) result = false; else result = true; @@ -293,7 +292,6 @@ static bool _TryReadIntPtr32(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) static bool _TryReadIntPtr(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) { bool result; - RuntimeHelpers.PrepareConstrainedRegions(); try { } @@ -310,7 +308,7 @@ static bool _TryReadIntPtr(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) ref res_len); readPtr = Marshal.ReadIntPtr(data); Marshal.FreeHGlobal(data); - if (!b || (int)res_len != dataSize) + if (!b || res_len.ToInt64() != dataSize) result = false; else result = true; @@ -321,7 +319,6 @@ static bool _TryReadIntPtr(IntPtr hProcess, IntPtr ptr, out IntPtr readPtr) static bool _TryReadIntPtrWow64(IntPtr hProcess, long ptr, out long readPtr) { bool result; - RuntimeHelpers.PrepareConstrainedRegions(); try { } @@ -360,11 +357,11 @@ static UniPtr _GetPenv(IntPtr hProcess) IntPtr ptr; if (!_TryReadIntPtr(hProcess, pPeb + 0x20, out ptr)) - throw new Exception("Unable to read PEB."); + throw new InvalidOperationException("Unable to read PEB."); IntPtr penv; if (!_TryReadIntPtr(hProcess, ptr + 0x80, out penv)) - throw new Exception("Unable to read RTL_USER_PROCESS_PARAMETERS."); + throw new InvalidOperationException("Unable to read RTL_USER_PROCESS_PARAMETERS."); return penv; } @@ -376,11 +373,11 @@ static UniPtr _GetPenv(IntPtr hProcess) long ptr; if (!_TryReadIntPtrWow64(hProcess, pPeb.ToInt64() + 0x20, out ptr)) - throw new Exception("Unable to read PEB."); + throw new InvalidOperationException("Unable to read PEB."); long penv; if (!_TryReadIntPtrWow64(hProcess, ptr + 0x80, out penv)) - throw new Exception("Unable to read RTL_USER_PROCESS_PARAMETERS."); + throw new InvalidOperationException("Unable to read RTL_USER_PROCESS_PARAMETERS."); return new UniPtr(penv); } @@ -393,11 +390,11 @@ static UniPtr _GetPenv(IntPtr hProcess) IntPtr ptr; if (!_TryReadIntPtr32(hProcess, pPeb + 0x10, out ptr)) - throw new Exception("Unable to read PEB."); + throw new InvalidOperationException("Unable to read PEB."); IntPtr penv; if (!_TryReadIntPtr32(hProcess, ptr + 0x48, out penv)) - throw new Exception("Unable to read RTL_USER_PROCESS_PARAMETERS."); + throw new InvalidOperationException("Unable to read RTL_USER_PROCESS_PARAMETERS."); return penv; } @@ -434,7 +431,7 @@ static IntPtr _GetPeb32(IntPtr hProcess) pbiSize, ref res_len); if (res_len != pbiSize) - throw new Exception("Unable to query process information."); + throw new InvalidOperationException("Unable to query process information."); return ptr; } else @@ -455,7 +452,7 @@ static IntPtr _GetPebNative(IntPtr hProcess) pbiSize, ref res_len); if (res_len != pbiSize) - throw new Exception("Unable to query process information."); + throw new InvalidOperationException("Unable to query process information."); return pbi.PebBaseAddress; } @@ -478,7 +475,7 @@ static UniPtr _GetPeb64(IntPtr hProcess) pbiSize, ref res_len); if (res_len != pbiSize) - throw new Exception("Unable to query process information."); + throw new InvalidOperationException("Unable to query process information."); return new UniPtr(pbi.PebBaseAddress); } } diff --git a/Shadowsocks.UnitTests/ProcessSingleInstanceGuardTests.cs b/Shadowsocks.UnitTests/ProcessSingleInstanceGuardTests.cs new file mode 100644 index 000000000..5cfd1414b --- /dev/null +++ b/Shadowsocks.UnitTests/ProcessSingleInstanceGuardTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class ProcessSingleInstanceGuardTests + { + [TestMethod] + public void NamedGateRejectsSecondThreadUntilOwnerReleasesIt() + { + string name = @"Local\Shadowsocks.Reborn.Tests." + Guid.NewGuid().ToString("N"); + using ProcessSingleInstanceGuard first = ProcessSingleInstanceGuard.TryAcquire(name); + Assert.IsNotNull(first); + + bool secondAcquired = TryAcquireOnWorker(name); + Assert.IsFalse(secondAcquired, "A concurrent process/thread must not acquire the same single-instance gate."); + + first.Dispose(); + + bool acquiredAfterRelease = TryAcquireOnWorker(name); + Assert.IsTrue(acquiredAfterRelease, "The single-instance gate must be reusable after the owner exits."); + } + + private static bool TryAcquireOnWorker(string name) + { + bool acquired = false; + Exception failure = null; + var thread = new Thread(() => + { + try + { + using ProcessSingleInstanceGuard guard = ProcessSingleInstanceGuard.TryAcquire(name); + acquired = guard is not null; + } + catch (Exception exception) + { + failure = exception; + } + }); + thread.Start(); + Assert.IsTrue(thread.Join(TimeSpan.FromSeconds(5)), "Worker thread did not finish while probing the single-instance gate."); + if (failure is not null) + { + throw failure; + } + return acquired; + } + } +} diff --git a/test/Properties/AssemblyInfo.cs b/Shadowsocks.UnitTests/Properties/AssemblyInfo.cs old mode 100755 new mode 100644 similarity index 74% rename from test/Properties/AssemblyInfo.cs rename to Shadowsocks.UnitTests/Properties/AssemblyInfo.cs index 26e77a341..f1599b2ee --- a/test/Properties/AssemblyInfo.cs +++ b/Shadowsocks.UnitTests/Properties/AssemblyInfo.cs @@ -1,36 +1,41 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("test")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("test")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("f74e87ac-7e3a-444b-a1d9-8b91a674c60f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +// This is a Windows-only desktop application. GenerateAssemblyInfo is disabled, +// so the SDK cannot emit this platform annotation automatically. +[assembly: SupportedOSPlatform("windows10.0.19041.0")] + + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Shadowsocks.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("shadowsocks-reborn")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f74e87ac-7e3a-444b-a1d9-8b91a674c60f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Shadowsocks.UnitTests/ProtocolHandlerTests.cs b/Shadowsocks.UnitTests/ProtocolHandlerTests.cs new file mode 100644 index 000000000..51354d16b --- /dev/null +++ b/Shadowsocks.UnitTests/ProtocolHandlerTests.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class ProtocolHandlerTests + { + [TestMethod] + public void ShellOpenCommandQuotesExecutableAndProtocolUrl() + { + const string executablePath = @"C:\Users\Example User\My Apps\Shadowsocks.exe"; + + string command = ProtocolHandler.BuildShellOpenCommand(executablePath); + + Assert.AreEqual( + "\"C:\\Users\\Example User\\My Apps\\Shadowsocks.exe\" --open-url \"%1\"", + command); + } + } +} diff --git a/Shadowsocks.UnitTests/SelfUpdaterTests.cs b/Shadowsocks.UnitTests/SelfUpdaterTests.cs new file mode 100644 index 000000000..738550742 --- /dev/null +++ b/Shadowsocks.UnitTests/SelfUpdaterTests.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class SelfUpdaterTests + { + [TestMethod] + public void InternalUpdateArgumentsAreRemovedBeforeNormalStartup() + { + string[] arguments = + { + "--start-visible", + SelfUpdater.CleanupSwitch, + SelfUpdater.ResumeVisibleSwitch, + SelfUpdater.TransactionOption, + @"C:\Temp\Shadowsocks\Updates\tx", + SelfUpdater.BackupOption + @"=C:\Apps\.Shadowsocks.exe.tx.update-backup", + SelfUpdater.UpdaterPidOption, + "1234", + SelfUpdater.PayloadSha256Option, + new string('a', 64), + "--open-url", + "ss://example", + }; + + string[] publicArguments = SelfUpdater.RemoveInternalArguments(arguments); + + CollectionAssert.AreEqual( + new[] { "--start-visible", "--open-url", "ss://example" }, + publicArguments); + } + + [TestMethod] + public void ResumeStartupOptionPreservesCurrentUiStateAndHiddenWinsConflicts() + { + Assert.AreEqual(AutoStartup.StartupHiddenOption, SelfUpdater.ResolveResumeStartupOption(resumeHidden: true, resumeVisible: false)); + Assert.AreEqual(AutoStartup.StartupVisibleOption, SelfUpdater.ResolveResumeStartupOption(resumeHidden: false, resumeVisible: true)); + Assert.AreEqual(AutoStartup.StartupHiddenOption, SelfUpdater.ResolveResumeStartupOption(resumeHidden: true, resumeVisible: true)); + Assert.AreEqual(string.Empty, SelfUpdater.ResolveResumeStartupOption(resumeHidden: false, resumeVisible: false)); + } + + [TestMethod] + public void TransactionDirectoryMustBeBelowSystemTempUpdateRoot() + { + string child = Path.Combine(SelfUpdater.UpdatesRoot, "test-transaction"); + Assert.IsTrue(SelfUpdater.IsCanonicalTransactionDirectory(child)); + Assert.IsFalse(SelfUpdater.IsCanonicalTransactionDirectory(SelfUpdater.UpdatesRoot)); + Assert.IsFalse(SelfUpdater.IsCanonicalTransactionDirectory(Path.GetTempPath())); + } + + [TestMethod] + public void UpdaterPathUsesSingleCanonicalExecutableName() + { + string transaction = Path.Combine(SelfUpdater.UpdatesRoot, "abc"); + string updater = SelfUpdater.GetTemporaryUpdaterPath(transaction); + Assert.AreEqual(SelfUpdater.TemporaryUpdaterFileName, Path.GetFileName(updater)); + } + + [TestMethod] + public void ReplacementRequiresStrictlyNewerPayloadVersion() + { + var installed = new Version(5, 2, 31, 0); + + Assert.IsFalse(SelfUpdater.IsStrictlyNewerReplacement(new Version(5, 2, 22, 0), installed)); + Assert.IsFalse(SelfUpdater.IsStrictlyNewerReplacement(new Version(5, 2, 31, 0), installed)); + Assert.IsTrue(SelfUpdater.IsStrictlyNewerReplacement(new Version(5, 2, 32, 0), installed)); + } + + [TestMethod] + public void StagedPayloadHashRejectsTampering() + { + string path = Path.Combine(Path.GetTempPath(), "shadowsocks-updater-hash-" + Guid.NewGuid().ToString("N") + ".exe"); + try + { + File.WriteAllBytes(path, new byte[] { 1, 2, 3, 4 }); + string expected = SelfUpdater.ComputeSha256Hex(path); + Assert.AreEqual(64, expected.Length); + + using (FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + SelfUpdater.VerifySha256(stream, expected); + } + + File.WriteAllBytes(path, new byte[] { 1, 2, 3, 5 }); + using FileStream tampered = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + Assert.ThrowsExactly(() => SelfUpdater.VerifySha256(tampered, expected)); + } + finally + { + File.Delete(path); + } + } + } +} diff --git a/Shadowsocks.UnitTests/Shadowsocks.UnitTests.csproj b/Shadowsocks.UnitTests/Shadowsocks.UnitTests.csproj new file mode 100644 index 000000000..44d45879c --- /dev/null +++ b/Shadowsocks.UnitTests/Shadowsocks.UnitTests.csproj @@ -0,0 +1,27 @@ + + + net10.0-windows10.0.19041.0 + Shadowsocks.UnitTests + Shadowsocks.UnitTests + true + true + x64 + x64 + win-x64 + false + disable + disable + + + + + + + + + + + + + + diff --git a/Shadowsocks.UnitTests/Sip003PluginTest.cs b/Shadowsocks.UnitTests/Sip003PluginTest.cs new file mode 100644 index 000000000..254b50138 --- /dev/null +++ b/Shadowsocks.UnitTests/Sip003PluginTest.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + [DoNotParallelize] + public class Sip003PluginTest + { + [TestInitialize] + public void Initialize() + { + PluginManager.PluginsDirectoryOverride = Path.Combine( + Path.GetTempPath(), + "Shadowsocks.UnitTests", + "Sip003", + Guid.NewGuid().ToString("N")); + } + + [TestCleanup] + public void Cleanup() + { + string root = PluginManager.PluginsDirectoryOverride; + PluginManager.PluginsDirectoryOverride = null; + try + { + if (!string.IsNullOrWhiteSpace(root) && Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + catch + { + } + } + + [TestMethod] + public void NoPluginReturnsNull() + { + Sip003Plugin plugin = Sip003Plugin.CreateIfConfigured( + new Server + { + server = "192.168.100.1", + ServerPort = 8888, + password = "test", + method = Server.DefaultMethod, + }, + false); + + Assert.IsNull(plugin); + } + + [TestMethod] + public void PluginMustBeInstalledByPluginManager() + { + var server = new Server + { + server = "192.168.100.1", + ServerPort = 8888, + password = "test", + method = Server.DefaultMethod, + plugin = "missing-plugin.exe", + }; + + FileNotFoundException exception = Assert.ThrowsExactly(() => + Sip003Plugin.CreateIfConfigured(server, false)); + + Assert.AreEqual("missing-plugin.exe", exception.FileName); + } + } +} diff --git a/Shadowsocks.UnitTests/Socks5ProxyTests.cs b/Shadowsocks.UnitTests/Socks5ProxyTests.cs new file mode 100644 index 000000000..11467f068 --- /dev/null +++ b/Shadowsocks.UnitTests/Socks5ProxyTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Proxy; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class Socks5ProxyTests + { + [TestMethod] + public async Task ConnectSupportsUsernamePasswordAndDomainReplyWithFragmentedFrames() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + + byte[] greeting = await ReadExactAsync(stream, 3); + CollectionAssert.AreEqual(new byte[] { 0x05, 0x01, 0x02 }, greeting); + await WriteFragmentedAsync(stream, new byte[] { 0x05, 0x02 }); + + byte[] authHeader = await ReadExactAsync(stream, 2); + Assert.AreEqual(0x01, authHeader[0]); + byte[] username = await ReadExactAsync(stream, authHeader[1]); + byte[] passwordLength = await ReadExactAsync(stream, 1); + byte[] password = await ReadExactAsync(stream, passwordLength[0]); + Assert.AreEqual("user", Encoding.UTF8.GetString(username)); + Assert.AreEqual("secret", Encoding.UTF8.GetString(password)); + await WriteFragmentedAsync(stream, new byte[] { 0x01, 0x00 }); + + byte[] requestHeader = await ReadExactAsync(stream, 5); + CollectionAssert.AreEqual(new byte[] { 0x05, 0x01, 0x00, 0x03 }, requestHeader[..4]); + int hostLength = requestHeader[4]; + byte[] host = await ReadExactAsync(stream, hostLength); + byte[] port = await ReadExactAsync(stream, 2); + Assert.AreEqual("example.com", Encoding.ASCII.GetString(host)); + Assert.AreEqual(443, (port[0] << 8) | port[1]); + + byte[] domain = Encoding.ASCII.GetBytes("proxy.local"); + var reply = new List { 0x05, 0x00, 0x00, 0x03, (byte)domain.Length }; + reply.AddRange(domain); + reply.Add(0x1F); + reply.Add(0x90); + await WriteFragmentedAsync(stream, reply.ToArray()); + }); + + var proxy = new Socks5Proxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await ConnectDestinationAsync( + proxy, + new DnsEndPoint("example.com", 443), + new NetworkCredential("user", "secret")); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + [TestMethod] + public async Task ConnectSupportsNoAuthenticationAndIpv6Reply() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + + CollectionAssert.AreEqual( + new byte[] { 0x05, 0x01, 0x00 }, + await ReadExactAsync(stream, 3)); + await WriteFragmentedAsync(stream, new byte[] { 0x05, 0x00 }); + + byte[] request = await ReadExactAsync(stream, 10); + CollectionAssert.AreEqual(new byte[] { 0x05, 0x01, 0x00, 0x01 }, request[..4]); + Assert.AreEqual(53, (request[8] << 8) | request[9]); + + var reply = new byte[4 + 16 + 2]; + reply[0] = 0x05; + reply[1] = 0x00; + reply[2] = 0x00; + reply[3] = 0x04; + IPAddress.IPv6Loopback.GetAddressBytes().CopyTo(reply, 4); + reply[^2] = 0x00; + reply[^1] = 0x35; + await WriteFragmentedAsync(stream, reply); + }); + + var proxy = new Socks5Proxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await ConnectDestinationAsync(proxy, new IPEndPoint(IPAddress.Parse("1.1.1.1"), 53), null); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + [TestMethod] + public async Task CredentialsRejectUnexpectedNoAuthenticationDowngrade() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + CollectionAssert.AreEqual(new byte[] { 0x05, 0x01, 0x02 }, await ReadExactAsync(stream, 3)); + await stream.WriteAsync(new byte[] { 0x05, 0x00 }); + }); + + var proxy = new Socks5Proxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await Assert.ThrowsExactlyAsync(() => + ConnectDestinationAsync( + proxy, + new DnsEndPoint("example.com", 443), + new NetworkCredential("user", "secret"))); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + [TestMethod] + public async Task UsernamePasswordFailureIsReported() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var listenerEndPoint = (IPEndPoint)listener.LocalEndpoint; + + Task serverTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + NetworkStream stream = client.GetStream(); + await ReadExactAsync(stream, 3); + await stream.WriteAsync(new byte[] { 0x05, 0x02 }); + + byte[] authHeader = await ReadExactAsync(stream, 2); + await ReadExactAsync(stream, authHeader[1]); + byte[] passwordLength = await ReadExactAsync(stream, 1); + await ReadExactAsync(stream, passwordLength[0]); + await stream.WriteAsync(new byte[] { 0x01, 0x01 }); + }); + + var proxy = new Socks5Proxy(); + try + { + await ConnectProxyAsync(proxy, listenerEndPoint); + await Assert.ThrowsExactlyAsync(() => + ConnectDestinationAsync( + proxy, + new DnsEndPoint("example.com", 443), + new NetworkCredential("user", "wrong"))); + await serverTask; + } + finally + { + proxy.Close(); + listener.Stop(); + } + } + + private static Task ConnectProxyAsync(Socks5Proxy proxy, EndPoint endpoint) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + proxy.BeginConnectProxy(endpoint, ar => + { + try + { + proxy.EndConnectProxy(ar); + completion.TrySetResult(); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null); + return completion.Task; + } + + private static Task ConnectDestinationAsync(Socks5Proxy proxy, EndPoint endpoint, NetworkCredential credential) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + proxy.BeginConnectDest(endpoint, ar => + { + try + { + proxy.EndConnectDest(ar); + completion.TrySetResult(); + } + catch (Exception ex) + { + completion.TrySetException(ex); + } + }, null, credential); + return completion.Task; + } + + private static async Task ReadExactAsync(NetworkStream stream, int count) + { + var buffer = new byte[count]; + int offset = 0; + while (offset < count) + { + int read = await stream.ReadAsync(buffer.AsMemory(offset, count - offset)); + if (read == 0) + { + throw new InvalidOperationException("Unexpected EOF in test SOCKS5 server."); + } + offset += read; + } + return buffer; + } + + private static async Task WriteFragmentedAsync(NetworkStream stream, byte[] buffer) + { + foreach (byte value in buffer) + { + await stream.WriteAsync(new byte[] { value }); + await Task.Yield(); + } + } + } +} diff --git a/Shadowsocks.UnitTests/StoragePolicyTest.cs b/Shadowsocks.UnitTests/StoragePolicyTest.cs new file mode 100644 index 000000000..2013ff17c --- /dev/null +++ b/Shadowsocks.UnitTests/StoragePolicyTest.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Core.Storage; +using Shadowsocks.Controller; +using Shadowsocks.Model; +using Shadowsocks.Windows.Shell; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + [DoNotParallelize] + public class StoragePolicyTest + { + [TestMethod] + public void NormalStorageUsesLocalAppDataForAllApplicationState() + { + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string temp = Path.GetTempPath(); + + Assert.IsTrue(AppStoragePaths.LocalAppDataRoot.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.StorageRoot.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.TempRoot.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.CleanSessionsRoot.StartsWith(temp, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.SettingsFile.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.PluginsDirectory.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual(Path.Combine(AppStoragePaths.StorageRoot, "Plugins"), AppStoragePaths.PluginsDirectory); + Assert.AreEqual(Path.Combine(AppStoragePaths.StorageRoot, "Components"), AppStoragePaths.ComponentsDirectory); + Assert.AreEqual(Path.Combine(AppStoragePaths.ComponentsDirectory, "DNSCryptProxy"), AppStoragePaths.DnsCryptComponentDirectory); + Assert.AreEqual(Path.Combine(AppStoragePaths.RuntimeRoot, "DNSCryptProxy"), AppStoragePaths.DnsCryptRuntimeDirectory); + Assert.AreEqual(Path.Combine(AppStoragePaths.TempUpdatesRoot, "DNSCryptProxy"), AppStoragePaths.DnsCryptUpdateDirectory); + Assert.IsTrue(AppStoragePaths.DnsCryptComponentDirectory.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(AppStoragePaths.StartupExecutableFile.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual("Shadowsocks.exe", Path.GetFileName(AppStoragePaths.StartupExecutableFile)); + } + + [TestMethod] + public void CleanModeUsesRufusStylePSuffix() + { + Assert.IsTrue(AppStoragePaths.IsCleanModeExecutableName(@"C:\Tools\Shadowsocksp.exe")); + Assert.IsTrue(AppStoragePaths.IsCleanModeExecutableName(@"C:\Tools\Shadowsocks-5.0p.exe")); + Assert.IsTrue(AppStoragePaths.IsCleanModeExecutableName(@"C:\Tools\SHADOWSOCKSP.EXE")); + Assert.IsFalse(AppStoragePaths.IsCleanModeExecutableName(@"C:\Tools\Shadowsocks.exe")); + Assert.IsFalse(AppStoragePaths.IsCleanModeExecutableName(@"C:\Tools\Shadowsocks-portable.exe")); + } + + [TestMethod] + public void JsonFileSettingsStorePersistsStructuredConfigurationAndBackup() + { + string root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + string file = Path.Combine(root, "settings.json"); + string backup = Path.Combine(root, "settings.backup.json"); + var store = new JsonFileSettingsStore(file, backup); + Configuration.ConfigureSettingsStore(store); + + Configuration first = new() { localPort = 1081, uiTheme = "Light" }; + Configuration.Save(first); + Configuration second = new() { localPort = 1082, uiTheme = "Dark" }; + Configuration.Save(second); + + string persisted = File.ReadAllText(file); + StringAssert.Contains(persisted, "\"Configuration\""); + StringAssert.Contains(persisted, "\"localPort\": 1082"); + StringAssert.Contains(persisted, "\"ConfigurationBackup\""); + Assert.IsTrue(File.Exists(backup)); + + Configuration loaded = Configuration.Load(); + Assert.AreEqual(1082, loaded.localPort); + Assert.AreEqual("Dark", loaded.uiTheme); + } + finally + { + Configuration.ConfigureSettingsStore(new MemorySettingsStore()); + try { Directory.Delete(root, recursive: true); } catch { } + } + } + + [TestMethod] + public void JsonFileSettingsStoreFallsBackToDurableBackupWhenPrimaryIsCorrupt() + { + string root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + string file = Path.Combine(root, "settings.json"); + string backup = Path.Combine(root, "settings.backup.json"); + var store = new JsonFileSettingsStore(file, backup); + + store.SetString("Value", "\"first\""); + store.SetString("Value", "\"second\""); + File.WriteAllText(file, "{ invalid json"); + + Assert.IsTrue(store.TryGetString("Value", out string recovered)); + Assert.AreEqual("first", recovered); + StringAssert.Contains(File.ReadAllText(file), "first"); + } + finally + { + try { Directory.Delete(root, recursive: true); } catch { } + } + } + + [TestMethod] + public void ConfigurationSchemaMigrationCanonicalizesUnknownProperties() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + store.SetString( + Configuration.SettingsValueName, + "{\"localPort\":1088,\"obsoleteProperty\":42,\"configs\":[],\"onlineConfigSource\":[]}"); + store.SetString( + Configuration.SettingsBackupValueName, + "{\"localPort\":1087,\"obsoleteProperty\":41,\"configs\":[],\"onlineConfigSource\":[]}"); + store.SetInt32(Configuration.SettingsSchemaVersionName, 1); + + Configuration loaded = Configuration.Load(); + + Assert.AreEqual(1088, loaded.localPort); + Assert.IsTrue(store.TryGetInt32(Configuration.SettingsSchemaVersionName, out int schemaVersion)); + Assert.AreEqual(Configuration.SettingsSchemaVersion, schemaVersion); + Assert.IsTrue(store.TryGetString(Configuration.SettingsValueName, out string primary)); + Assert.IsTrue(store.TryGetString(Configuration.SettingsBackupValueName, out string backup)); + Assert.IsFalse(primary.Contains("obsoleteProperty", StringComparison.Ordinal)); + Assert.IsFalse(backup.Contains("obsoleteProperty", StringComparison.Ordinal)); + } + + [TestMethod] + public void SchemaV2PersistedDnsCryptDefaultsArePreservedExactly() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + store.SetString( + Configuration.SettingsValueName, + "{\"configs\":[],\"dnsPolicy\":{\"mode\":0,\"dnsCrypt\":{\"autoUpdate\":true,\"requireDnssec\":true,\"requireNoLog\":true,\"requireNoFilter\":false,\"ipv4Servers\":true,\"ipv6Servers\":false,\"routeThroughShadowsocks\":false,\"automaticResolvers\":true,\"failClosed\":true,\"serverNames\":[]}}}"); + store.SetInt32(Configuration.SettingsSchemaVersionName, 2); + + Configuration loaded = Configuration.Load(); + + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.autoUpdate); + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.requireDnssec); + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.requireNoLog); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.requireNoFilter); + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.ipv4Servers); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.ipv6Servers); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.routeThroughShadowsocks); + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.automaticResolvers); + Assert.IsTrue(loaded.dnsPolicy.dnsCrypt.failClosed); + Assert.IsTrue(store.TryGetInt32(Configuration.SettingsSchemaVersionName, out int schemaVersion)); + Assert.AreEqual(Configuration.SettingsSchemaVersion, schemaVersion); + } + + [TestMethod] + public void SchemaV2CustomizedDnsCryptSettingsArePreserved() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + store.SetString( + Configuration.SettingsValueName, + "{\"configs\":[],\"dnsPolicy\":{\"mode\":0,\"dnsCrypt\":{\"autoUpdate\":false,\"requireDnssec\":true,\"requireNoLog\":true,\"requireNoFilter\":false,\"ipv4Servers\":true,\"ipv6Servers\":false,\"routeThroughShadowsocks\":false,\"automaticResolvers\":true,\"failClosed\":false,\"serverNames\":[]}}}"); + store.SetInt32(Configuration.SettingsSchemaVersionName, 2); + + Configuration loaded = Configuration.Load(); + Configuration.Process(ref loaded); + + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.autoUpdate); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.requireNoFilter); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.routeThroughShadowsocks); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.ipv6Servers); + Assert.IsFalse(loaded.dnsPolicy.dnsCrypt.failClosed); + } + + [TestMethod] + public void SaveWithoutServerPreservesDnsPreferenceButDisablesSystemProxy() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + Configuration configuration = new() + { + Enabled = true, + dnsPolicy = new Shadowsocks.Controller.Traffic.DnsPolicyConfig + { + mode = Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + }, + }; + + Configuration.Save(configuration); + Configuration loaded = Configuration.Load(); + Configuration.Process(ref loaded); + + Assert.IsFalse(loaded.Enabled); + Assert.IsFalse(loaded.HasConfiguredServer); + Assert.AreEqual( + Shadowsocks.Controller.Traffic.DnsPolicyMode.DnsCrypt, + loaded.dnsPolicy.mode); + } + + [TestMethod] + public void SavePreservesSelectedServerWhenOnlineGroupsAreSortedAfterLocalServers() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + + Server selectedSubscriptionServer = new() + { + server = "198.51.100.2", + ServerPort = 443, + password = "subscription-secret", + method = Server.DefaultMethod, + remarks = "Subscription server", + group = "https://example.invalid/subscription", + }; + Server localServer = new() + { + // Deliberately use the same endpoint as the selected subscription server. + // Server.Equals compares only host/port, so Save must track object identity. + server = "198.51.100.2", + ServerPort = 443, + password = "local-secret", + method = Server.DefaultMethod, + remarks = "Local server", + group = string.Empty, + }; + Configuration configuration = new() + { + configs = [selectedSubscriptionServer, localServer], + index = 0, + }; + + Configuration.Save(configuration); + + Assert.AreSame(localServer, configuration.configs[0]); + Assert.AreSame(selectedSubscriptionServer, configuration.configs[1]); + Assert.AreEqual(1, configuration.index); + Assert.AreSame(selectedSubscriptionServer, configuration.GetCurrentServer()); + } + + [TestMethod] + public void ConfigurationRoundTripsThroughSettingsStoreAndRollsBackCorruption() + { + MemorySettingsStore store = new(); + Configuration.ConfigureSettingsStore(store); + + Configuration first = new() { localPort = 1081, uiTheme = "Light" }; + Configuration.Save(first); + + Configuration second = new() { localPort = 1082, uiTheme = "Dark" }; + Configuration.Save(second); + + Configuration loaded = Configuration.Load(); + Assert.AreEqual(1082, loaded.localPort); + Assert.AreEqual("Dark", loaded.uiTheme); + Assert.IsTrue(store.TryGetString(Configuration.SettingsBackupValueName, out string backup)); + Assert.IsTrue(Configuration.TryDeserialize(backup, out Configuration backupConfig)); + Assert.AreEqual(1081, backupConfig.localPort); + + store.SetString(Configuration.SettingsValueName, "{ invalid json"); + Configuration rolledBack = Configuration.Load(); + Assert.AreEqual(1081, rolledBack.localPort); + Assert.IsTrue(store.TryGetString(Configuration.SettingsValueName, out string restoredPrimary)); + Assert.IsTrue(Configuration.TryDeserialize(restoredPrimary, out Configuration restoredConfig)); + Assert.AreEqual(1081, restoredConfig.localPort); + } + + private sealed class MemorySettingsStore : ISettingsStore + { + private readonly Dictionary values = new(StringComparer.Ordinal); + + public bool TryGetString(string name, out string value) + { + if (values.TryGetValue(name, out object raw) && raw is string text) + { + value = text; + return true; + } + value = null; + return false; + } + + public void SetString(string name, string value) => values[name] = value; + + public bool TryGetInt32(string name, out int value) + { + if (values.TryGetValue(name, out object raw) && raw is int number) + { + value = number; + return true; + } + value = default; + return false; + } + + public void SetInt32(string name, int value) => values[name] = value; + + public void DeleteValue(string name) => values.Remove(name); + } + [TestMethod] + public void StartupCopyResolvesOriginalExecutableForSelfUpdate() + { + string root = Path.Combine(Path.GetTempPath(), "ShadowsocksTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + string startup = Path.Combine(root, "Startup", "Shadowsocks.exe"); + string original = Path.Combine(root, "Shadowsocks-main.exe"); + Directory.CreateDirectory(Path.GetDirectoryName(startup)!); + File.WriteAllText(startup, "startup"); + File.WriteAllText(original, "original"); + + string resolved = AutoStartup.ResolvePrimaryExecutablePath( + startup, + startup, + new[] { AutoStartup.StartupHiddenOption, AutoStartup.StartupOriginOption, original }); + + Assert.AreEqual(Path.GetFullPath(original), resolved); + } + finally + { + if (Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + } + + [TestMethod] + public void StartupCopyRejectsMissingOriginForSelfUpdate() + { + string startup = Path.Combine(Path.GetTempPath(), "ShadowsocksTests", Guid.NewGuid().ToString("N"), "Shadowsocks.exe"); + string missing = Path.Combine(Path.GetDirectoryName(startup)!, "Shadowsocks-missing.exe"); + + string resolved = AutoStartup.ResolvePrimaryExecutablePath( + startup, + startup, + new[] { AutoStartup.StartupOriginOption, missing }); + + Assert.AreEqual(Path.GetFullPath(startup), resolved); + } + + [TestMethod] + public void StartupCommandMatcherRecognizesCanonicalAndLegacyRunEntries() + { + string path = Path.Combine(Path.GetTempPath(), "Shadowsocks Tests", "Shadowsocks.exe"); + + Assert.IsTrue(AutoStartup.CommandTargetsExecutable($"\"{path}\" --start-hidden", path)); + Assert.IsTrue(AutoStartup.CommandTargetsExecutable($"{path} --start-hidden", path)); + Assert.IsFalse(AutoStartup.CommandTargetsExecutable($"\"{path}.other\" --start-hidden", path)); + } + + [TestMethod] + public void RestartManagerRestoresHiddenState() + { + string commandLine = AutoStartup.BuildRestartCommandLine(Array.Empty(), windowVisible: false); + string[] arguments = WindowsCommandLine.ParseArguments(commandLine); + + CollectionAssert.Contains(arguments, AutoStartup.StartupHiddenOption); + CollectionAssert.DoesNotContain(arguments, AutoStartup.StartupVisibleOption); + } + + [TestMethod] + public void RestartManagerRestoresVisibleState() + { + string commandLine = AutoStartup.BuildRestartCommandLine(Array.Empty(), windowVisible: true); + string[] arguments = WindowsCommandLine.ParseArguments(commandLine); + + CollectionAssert.Contains(arguments, AutoStartup.StartupVisibleOption); + CollectionAssert.DoesNotContain(arguments, AutoStartup.StartupHiddenOption); + } + + [TestMethod] + public void StartupStateDetectionRecognizesBootArguments() + { + Assert.IsTrue(AutoStartup.IsHiddenStartup(new[] { AutoStartup.StartupHiddenOption })); + Assert.IsFalse(AutoStartup.IsHiddenStartup(new[] { AutoStartup.StartupVisibleOption })); + Assert.IsTrue(AutoStartup.IsVisibleStartup(new[] { AutoStartup.StartupVisibleOption })); + Assert.IsFalse(AutoStartup.IsVisibleStartup(new[] { AutoStartup.StartupHiddenOption })); + Assert.IsFalse(AutoStartup.IsHiddenStartup(Array.Empty())); + Assert.IsFalse(AutoStartup.IsVisibleStartup(Array.Empty())); + } + + [TestMethod] + public void RestartManagerReplacesStaleUiStateWithoutDuplication() + { + string commandLine = AutoStartup.BuildRestartCommandLine( + new[] + { + AutoStartup.StartupHiddenOption, + AutoStartup.StartupVisibleOption, + "value with spaces", + }, + windowVisible: true); + string[] arguments = WindowsCommandLine.ParseArguments(commandLine); + + Assert.AreEqual(0, arguments.Count(argument => string.Equals(argument, AutoStartup.StartupHiddenOption, StringComparison.OrdinalIgnoreCase))); + Assert.AreEqual(1, arguments.Count(argument => string.Equals(argument, AutoStartup.StartupVisibleOption, StringComparison.OrdinalIgnoreCase))); + CollectionAssert.Contains(arguments, "value with spaces"); + } + + } +} diff --git a/Shadowsocks.UnitTests/TrafficPolicyTest.cs b/Shadowsocks.UnitTests/TrafficPolicyTest.cs new file mode 100644 index 000000000..3fa06a507 --- /dev/null +++ b/Shadowsocks.UnitTests/TrafficPolicyTest.cs @@ -0,0 +1,483 @@ +using System.Collections.Generic; +using System.Net; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Strategy; +using Shadowsocks.Model; +using Shadowsocks.Routing; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class TrafficPolicyTest + { + [TestMethod] + public void BalancingStrategyHandlesNegativeUdpEndpointHash() + { + Server first = new() + { + server = "198.51.100.10", + ServerPort = 443, + password = "first", + method = Server.DefaultMethod, + }; + Server second = new() + { + server = "198.51.100.11", + ServerPort = 443, + password = "second", + method = Server.DefaultMethod, + }; + Configuration configuration = new() + { + configs = new List { first, second }, + }; + BalancingStrategy strategy = new(() => configuration); + + Server selected = strategy.GetAServer( + IStrategyCallerType.UDP, + new NegativeHashIpEndPoint(), + new IPEndPoint(IPAddress.Parse("203.0.113.10"), 53)); + + Assert.AreSame(second, selected); + } + + [TestMethod] + public void BalancingStrategySkipsIncompleteServers() + { + Server incomplete = new() + { + server = "198.51.100.20", + ServerPort = 443, + password = string.Empty, + method = Server.DefaultMethod, + }; + Server configured = new() + { + server = "198.51.100.21", + ServerPort = 443, + password = "configured", + method = Server.DefaultMethod, + }; + Configuration configuration = new() + { + configs = new List { incomplete, configured }, + }; + BalancingStrategy strategy = new(() => configuration); + + Assert.AreSame( + configured, + strategy.GetAServer( + IStrategyCallerType.UDP, + new ZeroHashIpEndPoint(), + new IPEndPoint(IPAddress.Parse("203.0.113.20"), 443))); + } + + [TestMethod] + public void HighAvailabilitySkipsIncompleteServers() + { + Server incomplete = new() + { + server = "198.51.100.30", + ServerPort = 443, + password = "damaged-timeout", + method = Server.DefaultMethod, + timeout = 0, + }; + Server configured = new() + { + server = "198.51.100.31", + ServerPort = 443, + password = "configured", + method = Server.DefaultMethod, + }; + Configuration configuration = new() + { + configs = new List { incomplete, configured }, + }; + HighAvailabilityStrategy strategy = new(() => configuration); + + strategy.ReloadServers(); + + Assert.AreSame( + configured, + strategy.GetAServer( + IStrategyCallerType.TCP, + new IPEndPoint(IPAddress.Loopback, 12345), + new IPEndPoint(IPAddress.Parse("203.0.113.30"), 443))); + } + + [TestMethod] + public void HighAvailabilityKeepsSameEndpointProfilesDistinct() + { + Server first = new() + { + server = "198.51.100.40", + ServerPort = 443, + password = "first", + method = Server.DefaultMethod, + }; + Server second = new() + { + server = "198.51.100.40", + ServerPort = 443, + password = "second", + method = Server.DefaultMethod, + }; + Configuration configuration = new() + { + configs = new List { first, second }, + }; + HighAvailabilityStrategy strategy = new(() => configuration); + strategy.ReloadServers(); + + var field = typeof(HighAvailabilityStrategy).GetField( + "_serverStatus", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + Assert.IsNotNull(field); + var statuses = (Dictionary)field.GetValue(strategy); + + Assert.AreEqual(2, statuses.Count); + Assert.IsTrue(statuses.ContainsKey(first)); + Assert.IsTrue(statuses.ContainsKey(second)); + } + + [TestMethod] + public void UserModeDefaultsToProxy() + { + Configuration configuration = new(); + TrafficPolicyEngine engine = new(configuration); + + RouteDecision decision = engine.EvaluateUserMode(new TrafficContext + { + ProcessName = "browser.exe", + DestinationHost = "example.com", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Proxy, decision.Action); + } + + [TestMethod] + public void ApplicationRuleOverridesUserMode() + { + Configuration configuration = new() + { + applicationRules = new List + { + new() + { + Application = "game*.exe", + Action = TrafficRouteAction.Direct, + }, + }, + }; + TrafficPolicyEngine engine = new(configuration); + + RouteDecision decision = engine.EvaluateUserMode(new TrafficContext + { + ProcessName = "game64", + DestinationHost = "example.com", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Direct, decision.Action); + } + + [TestMethod] + public void ApplicationRulesArePublishedAsImmutableSnapshots() + { + ApplicationRouteRule configuredRule = new() + { + Application = "browser.exe", + Action = TrafficRouteAction.Direct, + }; + Configuration configuration = new() + { + applicationRules = new List { configuredRule }, + }; + TrafficPolicyEngine engine = new(configuration); + + configuredRule.Application = "other.exe"; + configuredRule.Action = TrafficRouteAction.Block; + + RouteDecision decision = engine.EvaluateUserMode(new TrafficContext + { + ProcessName = "browser.exe", + DestinationHost = "example.com", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Direct, decision.Action); + } + + [TestMethod] + public void AdminModeUsesConfiguredFallbackWhenNoApplicationRuleMatches() + { + Configuration configuration = new(); + TrafficPolicyEngine engine = new(configuration); + + RouteDecision decision = engine.EvaluateAdminMode( + new TrafficContext + { + ProcessName = "unknown.exe", + DestinationPort = 443, + Protocol = "TCP", + }, + TrafficRouteAction.Direct); + + Assert.AreEqual(TrafficRouteAction.Direct, decision.Action); + } + + + [TestMethod] + public void ManagedRoutingSnapshotCanDirectOrProxyUserModeRequests() + { + Configuration configuration = new(); + TrafficPolicyEngine policy = new(configuration); + FilterEngine filters = FilterEngine.Compile( + new[] { "||proxy.example^", "@@||direct.example^" }, + System.Array.Empty(), + out FilterCompilationReport report); + policy.UpdateManagedRouting(ManagedRoutingSnapshot.LocalRules(filters, report)); + + RouteDecision proxy = policy.EvaluateUserMode(new TrafficContext + { + ProcessName = "browser.exe", + DestinationHost = "proxy.example", + DestinationUrl = "https://proxy.example/path", + DestinationPort = 443, + Protocol = "TCP", + }); + RouteDecision direct = policy.EvaluateUserMode(new TrafficContext + { + ProcessName = "browser.exe", + DestinationHost = "direct.example", + DestinationUrl = "https://direct.example/path", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Proxy, proxy.Action); + Assert.AreEqual(TrafficRouteAction.Direct, direct.Action); + Assert.AreEqual(1L, policy.ManagedProxyDecisionCount); + Assert.AreEqual(1L, policy.ManagedDirectDecisionCount); + StringAssert.Contains(proxy.Reason, "managed routing generation"); + } + + [TestMethod] + public void ApplicationRuleStillOverridesManagedRoutingSnapshot() + { + Configuration configuration = new() + { + applicationRules = new List + { + new() + { + Application = "browser.exe", + Action = TrafficRouteAction.Direct, + }, + }, + }; + TrafficPolicyEngine policy = new(configuration); + FilterEngine filters = FilterEngine.Compile( + new[] { "/.*/" }, + System.Array.Empty(), + out FilterCompilationReport report); + policy.UpdateManagedRouting(ManagedRoutingSnapshot.LocalRules(filters, report)); + + RouteDecision decision = policy.EvaluateUserMode(new TrafficContext + { + ProcessName = "browser.exe", + DestinationHost = "proxy.example", + DestinationUrl = "https://proxy.example/", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Direct, decision.Action); + Assert.AreEqual("application rule", decision.Reason); + } + + [TestMethod] + public void ManagedRoutingSnapshotPublicationReplacesWholeRuleSet() + { + TrafficPolicyEngine policy = new(new Configuration()); + FilterEngine first = FilterEngine.Compile( + new[] { "@@||switch.example^" }, + System.Array.Empty(), + out FilterCompilationReport firstReport); + FilterEngine second = FilterEngine.Compile( + new[] { "||switch.example^" }, + System.Array.Empty(), + out FilterCompilationReport secondReport); + ManagedRoutingSnapshot firstSnapshot = ManagedRoutingSnapshot.LocalRules(first, firstReport); + ManagedRoutingSnapshot secondSnapshot = ManagedRoutingSnapshot.LocalRules(second, secondReport); + + policy.UpdateManagedRouting(firstSnapshot); + RouteDecision before = policy.EvaluateUserMode(new TrafficContext + { + DestinationHost = "switch.example", + DestinationUrl = "https://switch.example/", + DestinationPort = 443, + Protocol = "TCP", + }); + policy.UpdateManagedRouting(secondSnapshot); + RouteDecision after = policy.EvaluateUserMode(new TrafficContext + { + DestinationHost = "switch.example", + DestinationUrl = "https://switch.example/", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.IsTrue(secondSnapshot.Generation > firstSnapshot.Generation); + Assert.AreEqual(TrafficRouteAction.Direct, before.Action); + Assert.AreEqual(TrafficRouteAction.Proxy, after.Action); + Assert.AreSame(secondSnapshot, policy.CurrentManagedRoutingSnapshot); + } + + [TestMethod] + public void ManagedRoutingWithoutUrlPreservesSystemProxyFallback() + { + TrafficPolicyEngine policy = new(new Configuration()); + FilterEngine filters = FilterEngine.Compile( + new[] { "@@||direct.example^" }, + System.Array.Empty(), + out FilterCompilationReport report); + policy.UpdateManagedRouting(ManagedRoutingSnapshot.LocalRules(filters, report)); + + RouteDecision decision = policy.EvaluateUserMode(new TrafficContext + { + DestinationHost = "direct.example", + DestinationPort = 443, + Protocol = "TCP", + }); + + Assert.AreEqual(TrafficRouteAction.Proxy, decision.Action); + } + + + [TestMethod] + public void AdminModeUsesManagedRoutingWhenAHostnameUrlIsAvailable() + { + TrafficPolicyEngine policy = new(new Configuration()); + FilterEngine filters = FilterEngine.Compile( + new[] { "||proxy.example^", "@@||direct.example^" }, + System.Array.Empty(), + out FilterCompilationReport report); + policy.UpdateManagedRouting(ManagedRoutingSnapshot.LocalRules(filters, report)); + + RouteDecision decision = policy.EvaluateAdminMode( + new TrafficContext + { + DestinationHost = "proxy.example", + DestinationUrl = "https://proxy.example/", + DestinationPort = 443, + Protocol = "TCP", + }, + TrafficRouteAction.Direct); + + Assert.AreEqual(TrafficRouteAction.Proxy, decision.Action); + StringAssert.Contains(decision.Reason, "managed routing generation"); + } + + + [TestMethod] + public void ManagedRoutingBuilderPreservesGlobalAndOnlinePacCompatibilityModes() + { + Configuration global = new() + { + Enabled = true, + global = true, + }; + Configuration onlinePac = new() + { + Enabled = true, + global = false, + useOnlinePac = true, + }; + + ManagedRoutingSnapshot globalSnapshot = ManagedRoutingSnapshotBuilder.Build(global); + ManagedRoutingSnapshot onlineSnapshot = ManagedRoutingSnapshotBuilder.Build(onlinePac); + + Assert.AreEqual(ManagedRoutingSnapshotMode.Disabled, globalSnapshot.Mode); + Assert.AreEqual(ManagedRoutingSnapshotMode.Disabled, onlineSnapshot.Mode); + StringAssert.Contains(globalSnapshot.Source, "global mode"); + StringAssert.Contains(onlineSnapshot.Source, "online PAC"); + } + + + [TestMethod] + public void ManagedHttpRoutingUrlPreservesHttpPathAndConnectAuthority() + { + Assert.AreEqual( + "http://example.com/path?q=1", + ManagedHttpProxyService.BuildRoutingUrlForPolicy( + "http://example.com/path?q=1", "example.com", 80, false)); + Assert.AreEqual( + "http://example.com/origin", + ManagedHttpProxyService.BuildRoutingUrlForPolicy( + "/origin", "example.com", 80, false)); + Assert.AreEqual( + "https://example.com:8443/", + ManagedHttpProxyService.BuildRoutingUrlForPolicy( + "example.com:8443", "example.com", 8443, true)); + Assert.AreEqual( + "https://[2001:db8::1]/", + ManagedHttpProxyService.BuildRoutingUrlForPolicy( + "[2001:db8::1]:443", "2001:db8::1", 443, true)); + } + + [TestMethod] + public void DnsPolicyIsPresentButDefaultsToSystem() + { + Configuration configuration = new(); + Assert.IsNotNull(configuration.dnsPolicy); + Assert.AreEqual(DnsPolicyMode.System, configuration.dnsPolicy.mode); + Assert.IsNotNull(configuration.dnsPolicy.dnsCrypt); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.autoUpdate); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.requireDnssec); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.requireNoLog); + Assert.IsTrue(configuration.dnsPolicy.dnsCrypt.failClosed); + } + + [TestMethod] + public void ConfigurationProcessRepairsMissingDnsCryptSettingsFromOlderJson() + { + Configuration configuration = new() + { + dnsPolicy = new DnsPolicyConfig { dnsCrypt = null }, + }; + + Configuration.Process(ref configuration); + + Assert.IsNotNull(configuration.dnsPolicy.dnsCrypt); + Assert.IsNotNull(configuration.dnsPolicy.dnsCrypt.serverNames); + } + + private sealed class ZeroHashIpEndPoint : IPEndPoint + { + public ZeroHashIpEndPoint() + : base(IPAddress.Loopback, 12345) + { + } + + public override int GetHashCode() => 0; + } + + private sealed class NegativeHashIpEndPoint : IPEndPoint + { + public NegativeHashIpEndPoint() + : base(IPAddress.Loopback, 12345) + { + } + + public override int GetHashCode() => -1; + } + } +} diff --git a/Shadowsocks.UnitTests/UdpRelayTests.cs b/Shadowsocks.UnitTests/UdpRelayTests.cs new file mode 100644 index 000000000..dc27fb4c6 --- /dev/null +++ b/Shadowsocks.UnitTests/UdpRelayTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class UdpRelayTests + { + [TestMethod] + public void Socks5UdpDestinationParsesIpv4DomainAndIpv6() + { + byte[] ipv4 = { 0, 0, 0, 1, 1, 1, 1, 1, 0, 53 }; + var ipv4Endpoint = UDPRelay.TryParseDestination(ipv4, ipv4.Length) as IPEndPoint; + Assert.IsNotNull(ipv4Endpoint); + Assert.AreEqual(IPAddress.Parse("1.1.1.1"), ipv4Endpoint.Address); + Assert.AreEqual(53, ipv4Endpoint.Port); + + byte[] host = Encoding.ASCII.GetBytes("example.com"); + var domain = new byte[3 + 1 + 1 + host.Length + 2]; + domain[3] = 3; + domain[4] = (byte)host.Length; + Buffer.BlockCopy(host, 0, domain, 5, host.Length); + domain[^2] = 0x01; + domain[^1] = 0xBB; + var domainEndpoint = UDPRelay.TryParseDestination(domain, domain.Length) as DnsEndPoint; + Assert.IsNotNull(domainEndpoint); + Assert.AreEqual("example.com", domainEndpoint.Host); + Assert.AreEqual(443, domainEndpoint.Port); + + byte[] ipv6Bytes = IPAddress.IPv6Loopback.GetAddressBytes(); + var ipv6 = new byte[3 + 1 + 16 + 2]; + ipv6[3] = 4; + Buffer.BlockCopy(ipv6Bytes, 0, ipv6, 4, ipv6Bytes.Length); + ipv6[^2] = 0x14; + ipv6[^1] = 0xE9; + var ipv6Endpoint = UDPRelay.TryParseDestination(ipv6, ipv6.Length) as IPEndPoint; + Assert.IsNotNull(ipv6Endpoint); + Assert.AreEqual(IPAddress.IPv6Loopback, ipv6Endpoint.Address); + Assert.AreEqual(5353, ipv6Endpoint.Port); + } + + [TestMethod] + public void Socks5UdpDestinationRejectsMalformedPackets() + { + Assert.IsNull(UDPRelay.TryParseDestination(new byte[] { 0, 0, 0, 3, 10, 1, 2 }, 7)); + Assert.IsNull(UDPRelay.TryParseDestination(new byte[] { 0, 0, 0, 9, 1, 2, 3, 4, 0, 53 }, 10)); + Assert.IsNull(UDPRelay.TryParseDestination(new byte[] { 0, 0, 0, 1, 1, 1, 1, 1, 0, 0 }, 10)); + } + } +} diff --git a/Shadowsocks.UnitTests/UpdateCheckerTests.cs b/Shadowsocks.UnitTests/UpdateCheckerTests.cs new file mode 100644 index 000000000..231f8c618 --- /dev/null +++ b/Shadowsocks.UnitTests/UpdateCheckerTests.cs @@ -0,0 +1,250 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using Shadowsocks.Controller; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class UpdateCheckerTests + { + [TestMethod] + public void StableReleaseWinsOverPrereleaseWithSameNumericVersion() + { + JArray releases = JArray.Parse(""" + [ + { "tag_name": "v5.3.0-beta.1", "prerelease": true, "draft": false }, + { "tag_name": "v5.3.0", "prerelease": false, "draft": false } + ] + """); + var configuration = new Configuration { checkPreRelease = true }; + + JToken selected = UpdateChecker.SelectLatestEligibleRelease( + releases, + configuration, + new Version(5, 2, 31, 0), + out Version version); + + Assert.IsNotNull(selected); + Assert.AreEqual("v5.3.0", (string)selected["tag_name"]); + Assert.AreEqual(new Version(5, 3, 0, 0), version); + } + + [TestMethod] + public void OlderAndEqualReleasesAreNotEligibleForInstalledVersion() + { + JArray releases = JArray.Parse(""" + [ + { "tag_name": "v5.2.22", "prerelease": false, "draft": false }, + { "tag_name": "v5.2.31", "prerelease": false, "draft": false } + ] + """); + + JToken selected = UpdateChecker.SelectLatestEligibleRelease( + releases, + new Configuration(), + new Version(5, 2, 31, 0), + out Version version); + + Assert.IsNull(selected); + Assert.IsNull(version); + } + + [TestMethod] + public void OnlyStrictlyNewerReleaseCanUpdateInstalledVersion() + { + JArray releases = JArray.Parse(""" + [ + { "tag_name": "v5.2.22", "prerelease": false, "draft": false }, + { "tag_name": "v5.2.31", "prerelease": false, "draft": false }, + { "tag_name": "v5.2.32", "prerelease": false, "draft": false } + ] + """); + + JToken selected = UpdateChecker.SelectLatestEligibleRelease( + releases, + new Configuration(), + new Version(5, 2, 31, 0), + out Version version); + + Assert.IsNotNull(selected); + Assert.AreEqual("v5.2.32", (string)selected["tag_name"]); + Assert.AreEqual(new Version(5, 2, 32, 0), version); + } + + [TestMethod] + public void NewerPrimaryExecutableWinsOverStaleStartupCopyVersion() + { + Version effective = UpdateChecker.SelectEffectiveInstalledVersion( + new Version(5, 2, 21, 0), + new Version(5, 2, 31, 0)); + + Assert.AreEqual(new Version(5, 2, 31, 0), effective); + + JArray releases = JArray.Parse(""" + [ + { "tag_name": "v5.2.22", "prerelease": false, "draft": false } + ] + """); + + JToken selected = UpdateChecker.SelectLatestEligibleRelease( + releases, + new Configuration(), + effective, + out Version version); + + Assert.IsNull(selected); + Assert.IsNull(version); + } + + [TestMethod] + public void SkippedReleaseDoesNotStopSearchForNewerVersion() + { + JArray releases = JArray.Parse(""" + [ + { "tag_name": "v5.4.0", "prerelease": false, "draft": false }, + { "tag_name": "v5.3.0", "prerelease": false, "draft": false } + ] + """); + var configuration = new Configuration { skippedUpdateVersion = "v5.3.0" }; + + JToken selected = UpdateChecker.SelectLatestEligibleRelease( + releases, + configuration, + new Version(5, 2, 31, 0), + out Version version); + + Assert.IsNotNull(selected); + Assert.AreEqual("v5.4.0", (string)selected["tag_name"]); + Assert.AreEqual(new Version(5, 4, 0, 0), version); + } + + [TestMethod] + public void CanonicalAssetNameIsRequiredExactly() + { + JArray assets = JArray.Parse(""" + [ + { "name": "folder/Shadowsocks-win-x64.zip" }, + { "name": "other-win-x64.zip" } + ] + """); + + Assert.IsNull(UpdateChecker.SelectReleaseZipAsset(assets)); + + assets.Add(new JObject { ["name"] = UpdateChecker.PreferredReleaseZipFilename }); + Assert.IsNotNull(UpdateChecker.SelectReleaseZipAsset(assets)); + } + + [TestMethod] + public void ReleaseAssetOriginMustBeCanonicalRepository() + { + Assert.IsTrue(UpdateChecker.IsAllowedReleaseDownloadUrl( + "https://github.com/SlimRG/shadowsocks-reborn/releases/download/v5.2.32/Shadowsocks-win-x64.zip")); + Assert.IsFalse(UpdateChecker.IsAllowedReleaseDownloadUrl( + "https://example.com/SlimRG/shadowsocks-reborn/releases/download/v5.2.32/Shadowsocks-win-x64.zip")); + Assert.IsFalse(UpdateChecker.IsAllowedReleaseDownloadUrl( + "https://github.com/Other/repo/releases/download/v5.2.32/Shadowsocks-win-x64.zip")); + } + + [TestMethod] + public async Task Sha256SidecarAcceptsPlainDigestAndValidatesOptionalCanonicalFileName() + { + string root = CreateTempDirectory(); + try + { + string payload = Path.Combine(root, UpdateChecker.PreferredReleaseZipFilename); + await File.WriteAllBytesAsync(payload, new byte[] { 1, 2, 3, 4 }); + string hash = Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(payload))).ToLowerInvariant(); + string checksum = payload + ".sha256"; + + await File.WriteAllTextAsync(checksum, $"{hash} folder/{UpdateChecker.PreferredReleaseZipFilename}"); + await Assert.ThrowsExactlyAsync(() => + UpdateChecker.VerifySha256Async(payload, checksum, UpdateChecker.PreferredReleaseZipFilename)); + + await File.WriteAllTextAsync(checksum, $"{hash} {UpdateChecker.PreferredReleaseZipFilename}"); + await UpdateChecker.VerifySha256Async(payload, checksum, UpdateChecker.PreferredReleaseZipFilename); + + await File.WriteAllTextAsync(checksum, hash); + await UpdateChecker.VerifySha256Async(payload, checksum, UpdateChecker.PreferredReleaseZipFilename); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [TestMethod] + public async Task DownloadedAssetIsClosedBeforePromotion() + { + string root = CreateTempDirectory(); + try + { + string destination = Path.Combine(root, UpdateChecker.PreferredReleaseZipFilename); + await File.WriteAllTextAsync(destination, "old"); + byte[] expected = { 0x53, 0x53, 0x2D, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45 }; + using var content = new System.Net.Http.ByteArrayContent(expected); + + await UpdateChecker.WriteDownloadedAssetAsync(content, destination); + + CollectionAssert.AreEqual(expected, await File.ReadAllBytesAsync(destination)); + Assert.IsFalse(File.Exists(destination + ".download")); + + using FileStream exclusiveProbe = new(destination, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + Assert.AreEqual(expected.Length, exclusiveProbe.Length); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [TestMethod] + public void UpdateZipMustContainOneRootExecutableOnly() + { + string root = CreateTempDirectory(); + try + { + string validZip = Path.Combine(root, "valid.zip"); + using (ZipArchive archive = ZipFile.Open(validZip, ZipArchiveMode.Create)) + { + ZipArchiveEntry entry = archive.CreateEntry("Shadowsocks.exe"); + using Stream output = entry.Open(); + output.WriteByte(0x4D); + output.WriteByte(0x5A); + } + + string extracted = Path.Combine(root, "Shadowsocks.Update.exe"); + UpdateChecker.ExtractCanonicalExecutable(validZip, extracted); + Assert.IsTrue(File.Exists(extracted)); + + string invalidZip = Path.Combine(root, "invalid.zip"); + using (ZipArchive archive = ZipFile.Open(invalidZip, ZipArchiveMode.Create)) + { + ZipArchiveEntry entry = archive.CreateEntry("bin/Shadowsocks.exe"); + using Stream output = entry.Open(); + output.WriteByte(1); + } + + Assert.ThrowsExactly(() => + UpdateChecker.ExtractCanonicalExecutable(invalidZip, Path.Combine(root, "bad.exe"))); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + private static string CreateTempDirectory() + { + string path = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", "UpdateChecker", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } + } +} diff --git a/Shadowsocks.UnitTests/UrlTest.cs b/Shadowsocks.UnitTests/UrlTest.cs new file mode 100644 index 000000000..8727859c7 --- /dev/null +++ b/Shadowsocks.UnitTests/UrlTest.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Shadowsocks.Model; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class UrlTest + { + private const string BaseUserInfo = "Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0"; + + [TestMethod] + public void Sip002UrlRoundTripsWithoutPlugin() + { + var expected = new Server + { + server = "192.168.100.1", + ServerPort = 8888, + password = "test", + method = Server.DefaultMethod, + remarks = "example-server 1", + }; + + string url = $"ss://{BaseUserInfo}@192.168.100.1:8888/#example-server+1"; + Server actual = Server.ParseURL(url); + + AssertServerEquals(expected, actual); + Assert.IsTrue(actual.importedFromUrl); + Assert.AreEqual(url, actual.GetURL()); + } + + [TestMethod] + public void Sip002UrlRoundTripsWithPlugin() + { + var expected = new Server + { + server = "192.168.1.1", + ServerPort = 8388, + password = "test", + method = Server.DefaultMethod, + plugin = "v2ray-plugin", + PluginOptions = "mode=websocket;host=example.com", + }; + + string url = $"ss://{BaseUserInfo}@192.168.1.1:8388/?plugin=v2ray-plugin%3bmode%3dwebsocket%3bhost%3dexample.com"; + Server actual = Server.ParseURL(url); + + AssertServerEquals(expected, actual); + Assert.AreEqual(url, actual.GetURL()); + } + + + [TestMethod] + public void ServerUsesStableJsonPropertyNames() + { + Server server = new() + { + ServerPort = 8388, + PluginOptions = "mode=websocket", + PluginArguments = "--fast-open", + }; + + string json = JsonConvert.SerializeObject(server); + Server roundTrip = JsonConvert.DeserializeObject(json); + + StringAssert.Contains(json, "\"server_port\":8388"); + StringAssert.Contains(json, "\"plugin_opts\":\"mode=websocket\""); + StringAssert.Contains(json, "\"plugin_args\":\"--fast-open\""); + Assert.IsFalse(json.Contains("\"ServerPort\"", StringComparison.Ordinal)); + Assert.IsFalse(json.Contains("\"PluginOptions\"", StringComparison.Ordinal)); + Assert.IsFalse(json.Contains("\"PluginArguments\"", StringComparison.Ordinal)); + Assert.IsNotNull(roundTrip); + Assert.AreEqual(8388, roundTrip.ServerPort); + Assert.AreEqual("mode=websocket", roundTrip.PluginOptions); + Assert.AreEqual("--fast-open", roundTrip.PluginArguments); + } + + [TestMethod] + public void MultipleSip002UrlsAreParsed() + { + string first = $"ss://{BaseUserInfo}@192.168.100.1:8888/"; + string second = $"ss://{BaseUserInfo}@192.168.1.1:8388/"; + + var servers = Server.GetServers(first + "\r\n" + second); + + Assert.AreEqual(2, servers.Count); + Assert.AreEqual("192.168.100.1", servers[0].server); + Assert.AreEqual("192.168.1.1", servers[1].server); + } + + [TestMethod] + public void PreSip002UrlIsRejected() + { + Assert.IsNull(Server.ParseURL("ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4")); + } + + private static void AssertServerEquals(Server expected, Server actual) + { + Assert.IsNotNull(actual); + Assert.AreEqual(expected.server, actual.server); + Assert.AreEqual(expected.ServerPort, actual.ServerPort); + Assert.AreEqual(expected.password, actual.password); + Assert.AreEqual(expected.method, actual.method); + Assert.AreEqual(expected.plugin, actual.plugin); + Assert.AreEqual(expected.PluginOptions, actual.PluginOptions); + Assert.AreEqual(expected.remarks, actual.remarks); + } + } +} diff --git a/Shadowsocks.UnitTests/WinDivertInstallerTests.cs b/Shadowsocks.UnitTests/WinDivertInstallerTests.cs new file mode 100644 index 000000000..e98ec450d --- /dev/null +++ b/Shadowsocks.UnitTests/WinDivertInstallerTests.cs @@ -0,0 +1,208 @@ +#nullable enable +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.UnitTests +{ + [TestClass] + public class WinDivertInstallerTests + { + [TestMethod] + public void SecurityPinsMatchReviewedWinDivertRelease() + { + Assert.AreEqual("2.2.2", ReadWinDivertConstant(nameof(WinDivertInstaller.Version))); + Assert.AreEqual( + "https://github.com/basil00/WinDivert/releases/download/v2.2.2/WinDivert-2.2.2-A.zip", + ReadWinDivertConstant(nameof(WinDivertInstaller.PackageUrl))); + Assert.AreEqual( + "c1e060ee19444a259b2162f8af0f3fe8c4428a1c6f694dce20de194ac8d7d9a2", + ReadWinDivertConstant(nameof(WinDivertInstaller.DllSha256))); + Assert.AreEqual( + "8da085332782708d8767bcace5327a6ec7283c17cfb85e40b03cd2323a90ddc2", + ReadWinDivertConstant(nameof(WinDivertInstaller.DriverSha256))); + } + + private static string ReadWinDivertConstant(string fieldName) + { + System.Reflection.FieldInfo field = typeof(WinDivertInstaller).GetField( + fieldName, + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static) + ?? throw new AssertFailedException($"Missing public constant '{fieldName}'."); + return (string)(field.GetRawConstantValue() + ?? throw new AssertFailedException($"Constant '{fieldName}' has no value.")); + } + + [TestMethod] + public void ExtractRuntimeAcceptsOnlyExpectedX64Entries() + { + string root = CreateTempDirectory(); + try + { + byte[] dll = CreatePe(0x8664); + byte[] driver = CreatePe(0x8664); + byte[] package = CreatePackage(dll, driver); + + WinDivertInstaller.ExtractRuntime( + package, + root, + Sha256(dll), + Sha256(driver)); + + Assert.IsTrue(File.Exists(Path.Combine(root, "WinDivert.dll"))); + Assert.IsTrue(File.Exists(Path.Combine(root, "WinDivert64.sys"))); + WinDivertInstaller.ValidateX64PortableExecutable(Path.Combine(root, "WinDivert.dll")); + WinDivertInstaller.ValidateX64PortableExecutable(Path.Combine(root, "WinDivert64.sys")); + } + finally + { + TryDelete(root); + } + } + + [TestMethod] + public void ExtractRuntimeRejectsWrongArchitecture() + { + string root = CreateTempDirectory(); + try + { + byte[] dll = CreatePe(0x014c); + byte[] driver = CreatePe(0x8664); + byte[] package = CreatePackage(dll, driver); + Assert.ThrowsExactly(() => WinDivertInstaller.ExtractRuntime( + package, + root, + Sha256(dll), + Sha256(driver))); + } + finally + { + TryDelete(root); + } + } + + [TestMethod] + public void ExtractRuntimeRejectsUnexpectedArchiveLayout() + { + string root = CreateTempDirectory(); + try + { + using var buffer = new MemoryStream(); + using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true)) + { + WriteEntry(archive, "unexpected/x64/WinDivert.dll", CreatePe(0x8664)); + WriteEntry(archive, "unexpected/x64/WinDivert64.sys", CreatePe(0x8664)); + } + + Assert.ThrowsExactly(() => WinDivertInstaller.ExtractRuntime( + buffer.ToArray(), + root, + new string('0', 64), + new string('0', 64))); + } + finally + { + TryDelete(root); + } + } + + + [TestMethod] + public void ExtractRuntimeRejectsPinnedHashMismatch() + { + string root = CreateTempDirectory(); + try + { + byte[] dll = CreatePe(0x8664); + byte[] driver = CreatePe(0x8664); + byte[] package = CreatePackage(dll, driver); + + Assert.ThrowsExactly(() => WinDivertInstaller.ExtractRuntime( + package, + root, + new string('0', 64), + Sha256(driver))); + Assert.IsFalse(File.Exists(Path.Combine(root, "WinDivert.dll"))); + } + finally + { + TryDelete(root); + } + } + + [TestMethod] + public void ValidatePinnedRuntimeFileRejectsTamperedCache() + { + string root = CreateTempDirectory(); + try + { + string path = Path.Combine(root, "WinDivert.dll"); + byte[] image = CreatePe(0x8664); + File.WriteAllBytes(path, image); + string expected = Sha256(image); + + WinDivertInstaller.ValidatePinnedRuntimeFile(path, expected); + image[^1] ^= 0x5A; + File.WriteAllBytes(path, image); + + Assert.ThrowsExactly(() => + WinDivertInstaller.ValidatePinnedRuntimeFile(path, expected)); + } + finally + { + TryDelete(root); + } + } + + private static byte[] CreatePackage(byte[] dll, byte[] driver) + { + using var buffer = new MemoryStream(); + using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true)) + { + const string root = "WinDivert-2.2.2-A/x64/"; + WriteEntry(archive, root + "WinDivert.dll", dll); + WriteEntry(archive, root + "WinDivert64.sys", driver); + } + return buffer.ToArray(); + } + + private static void WriteEntry(ZipArchive archive, string path, byte[] content) + { + ZipArchiveEntry entry = archive.CreateEntry(path, CompressionLevel.Fastest); + using Stream output = entry.Open(); + output.Write(content, 0, content.Length); + } + + private static byte[] CreatePe(ushort machine) + { + byte[] image = new byte[128]; + image[0] = (byte)'M'; + image[1] = (byte)'Z'; + BitConverter.GetBytes(0x40).CopyTo(image, 0x3C); + image[0x40] = (byte)'P'; + image[0x41] = (byte)'E'; + BitConverter.GetBytes(machine).CopyTo(image, 0x44); + return image; + } + + private static string Sha256(byte[] content) + { + return Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); + } + + private static string CreateTempDirectory() + { + string root = Path.Combine(Path.GetTempPath(), "Shadowsocks.UnitTests", "WinDivert", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + return root; + } + + private static void TryDelete(string path) + { + try { Directory.Delete(path, recursive: true); } catch { } + } + } +} diff --git a/Shadowsocks.WinUI/App.cs b/Shadowsocks.WinUI/App.cs new file mode 100644 index 000000000..6e0d85fa4 --- /dev/null +++ b/Shadowsocks.WinUI/App.cs @@ -0,0 +1,1256 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.AppLifecycle; +using NLog; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Core; +using Shadowsocks.Core.Logging; +using Shadowsocks.Core.Storage; +using Shadowsocks.Model; +using Shadowsocks.Localization; +using Shadowsocks.WinUI.Shell; +using Shadowsocks.Windows.Shell; +using Shadowsocks.Windows.WinUI.Shell; +using Shadowsocks.Windows.Storage; +using Windows.ApplicationModel.Activation; +using Windows.ApplicationModel.DataTransfer; + +namespace Shadowsocks.WinUI; + +public sealed partial class App : Application, IDisposable +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private static AppInstance? s_startupAppInstance; + private static AppActivationArguments? s_startupActivation; + private static ILocalizationService? s_startupLocalization; + + private readonly AppInstance _appInstance; + private readonly AppActivationArguments _initialActivation; + private readonly ILocalizationService _localization; + + private DispatcherQueue? _dispatcherQueue; + private ShadowsocksController? _controller; + private MainWindow? _window; + private TrayIconService? _trayIcon; + private WinUIHotkeyManager? _hotkeyManager; + private NativeUserInteractionService? _userInteraction; + private PowerModeMonitor? _powerModeMonitor; + private UpdateChecker? _startupUpdateChecker; + private readonly CancellationTokenSource _lifetimeCancellation = new(); + private CancellationTokenSource? _resumeRestartCancellation; + private readonly ConcurrentQueue _pendingControllerErrors = new(); + private bool _controllerErrorDialogActive; + private bool _alreadyRunningDialogActive; + private bool _shuttingDown; + private bool _systemSessionEnding; + private bool _disposed; + + public App() + { + _appInstance = s_startupAppInstance + ?? throw new InvalidOperationException("WinUI startup context was not configured before App creation."); + _initialActivation = s_startupActivation + ?? throw new InvalidOperationException("WinUI activation context was not configured before App creation."); + _localization = s_startupLocalization + ?? throw new InvalidOperationException("WinUI localization service was not configured before App creation."); + + s_startupAppInstance = null; + s_startupActivation = null; + s_startupLocalization = null; + + AppStoragePaths.EnsureStorageDirectories(); + LoggingConfigurator.Configure(); + WindowsStorageBootstrapper.Initialize(); + Directory.SetCurrentDirectory(AppStoragePaths.RuntimeRoot); + + try + { + InitializeComponent(); + } + catch (Exception exception) + { + Logger.Error(exception, "Failed to initialize the WinUI application resource dictionary."); + throw; + } + + UnhandledException += OnUnhandledException; + } + + internal static void ConfigureStartupContext( + AppInstance appInstance, + AppActivationArguments initialActivation, + ILocalizationService localization) + { + ArgumentNullException.ThrowIfNull(appInstance); + ArgumentNullException.ThrowIfNull(initialActivation); + ArgumentNullException.ThrowIfNull(localization); + + if (s_startupAppInstance is not null || s_startupActivation is not null || s_startupLocalization is not null) + { + throw new InvalidOperationException("WinUI startup context has already been configured."); + } + + s_startupAppInstance = appInstance; + s_startupActivation = initialActivation; + s_startupLocalization = localization; + } + + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + Program.RegisterActivationHandler(OnRedirectedActivation); + + try + { + _userInteraction = new NativeUserInteractionService(); + _controller = new ShadowsocksController(_userInteraction); + SubscribeControllerEvents(_controller); + _controller.Start(); + } + catch (Exception exception) + { + _pendingControllerErrors.Enqueue(exception); + Logger.Error(exception, "Failed to start the Shadowsocks controller from the WinUI shell."); + } + + _window = new MainWindow( + _controller, + AutoStartup.Check, + SetStartWithWindowsAsync, + RegisterHotkeys, + ApplyHotkeys, + ShutdownAndExit, + OnWindowVisibilityChanged, + _localization); + if (_userInteraction is not null) + { + _userInteraction.OwnerWindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(_window); + } + if (_window.Content is FrameworkElement contentRoot) + { + contentRoot.Loaded += OnMainWindowContentLoaded; + } + try + { + InitializeSystemShell(); + } + catch (Exception exception) + { + Logger.Error(exception, "Failed to initialize one or more WinUI system-shell services."); + _window.SetShellStatus(_localization.Format("Shell initialization warning: {0}", exception.Message), InfoBarSeverity.Warning); + } + + try + { + InitializePowerModeMonitor(); + } + catch (Exception exception) + { + Logger.Error(exception, "Failed to initialize Windows suspend/resume monitoring."); + _window.SetShellStatus(_localization.Format("Shell initialization warning: {0}", exception.Message), InfoBarSeverity.Warning); + } + + bool startHidden = AutoStartup.IsHiddenStartup(RuntimeEnvironment.Arguments); + bool startVisible = !startHidden && AutoStartup.IsVisibleStartup(RuntimeEnvironment.Arguments); + if (startHidden) + { + // Creating a WinUI Window materializes an HWND even before Activate(). + // Explicitly hide it when the previous session ended in the notification area. + _window.HideToTray(); + } + + ScheduleStartupOnlineConfigRefresh(); + + Configuration? startupConfiguration = _controller?.GetCurrentConfiguration(); + bool firstRun = startupConfiguration?.firstRun == true; + if (firstRun && !startHidden) + { + // Match the original Shadowsocks startup behavior: only the first run + // opens configuration. A restored hidden state still takes precedence. + _window.NavigateToServers(); + _window.ShowFromTray(); + } + else if (startVisible) + { + _window.ShowFromTray(); + } + + if (!firstRun && startupConfiguration?.autoCheckUpdate == true) + { + StartAutomaticUpdateCheck(); + } + + ProcessActivation(_initialActivation, showWindow: false); + + // Controller startup errors (for example, an occupied local port) still + // need a visible XamlRoot so the user sees the same explicit error flow + // as expected by the controller contract. This is an exceptional path, not normal startup. + if (!_pendingControllerErrors.IsEmpty) + { + ShowMainWindow(); + } + + // Whichever Windows mechanism owns the next reboot restore now records the + // actual shell state after first-run and activation handling have completed. + if (!AppStoragePaths.IsCleanMode) + { + AutoStartup.SynchronizeUiState(_window.IsVisibleToUser); + } + } + + private void InitializePowerModeMonitor() + { + if (_window is null) + { + throw new InvalidOperationException("The main WinUI window is not initialized."); + } + + _powerModeMonitor = new PowerModeMonitor(_window); + _powerModeMonitor.SessionEnding += OnSystemSessionEnding; + _powerModeMonitor.SessionEndCancelled += OnSystemSessionEndCancelled; + _powerModeMonitor.Suspending += OnSystemSuspending; + _powerModeMonitor.Resumed += OnSystemResumed; + } + + private void OnSystemSessionEnding(object? sender, EventArgs e) + { + if (_systemSessionEnding) + { + return; + } + + _systemSessionEnding = true; + + // WM_QUERYENDSESSION arrives before Windows closes the top-level window. + // Snapshot the real pre-shutdown visibility now; any later Closing event must + // neither turn an open-window state into "hidden" nor cancel Windows shutdown. + if (_window is not null) + { + bool windowVisible = _window.IsVisibleToUser; + _window.PrepareForSystemSessionEnd(); + if (!AppStoragePaths.IsCleanMode) + { + AutoStartup.SynchronizeUiState(windowVisible); + } + } + } + + private void OnSystemSessionEndCancelled(object? sender, EventArgs e) + { + if (!_systemSessionEnding) + { + return; + } + + _systemSessionEnding = false; + _window?.CancelSystemSessionEnd(); + + if (!AppStoragePaths.IsCleanMode && _window is not null) + { + AutoStartup.SynchronizeUiState(_window.IsVisibleToUser); + } + } + + private void OnSystemSuspending(object? sender, EventArgs e) + { + _resumeRestartCancellation?.Cancel(); + _resumeRestartCancellation?.Dispose(); + _resumeRestartCancellation = null; + + ShadowsocksController? controller = _controller; + if (controller is null || _shuttingDown) + { + return; + } + + try + { + controller.Stop(); + Logger.Info("Controller stopped because Windows is suspending."); + } + catch (Exception exception) + { + Logger.Error(exception, "Controller stop failed while Windows was suspending."); + } + } + + private void OnSystemResumed(object? sender, EventArgs e) + { + if (_controller is null || _shuttingDown) + { + return; + } + + _resumeRestartCancellation?.Cancel(); + _resumeRestartCancellation?.Dispose(); + _resumeRestartCancellation = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCancellation.Token); + CancellationToken cancellationToken = _resumeRestartCancellation.Token; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested || _shuttingDown) + { + return; + } + + ShadowsocksController? controller = _controller; + if (controller is null) + { + return; + } + + controller.Start(systemWakeUp: true); + Logger.Info("Controller restarted after Windows resumed."); + } + catch (OperationCanceledException) + { + // Normal during a second suspend/resume cycle or application shutdown. + } + catch (Exception exception) + { + Logger.Error(exception, "Controller restart failed after Windows resumed."); + _pendingControllerErrors.Enqueue(exception); + _ = _dispatcherQueue?.TryEnqueue(() => + { + ShowMainWindow(); + _ = DrainControllerErrorQueueAsync(); + }); + } + }, cancellationToken); + } + + private void ScheduleStartupOnlineConfigRefresh() + { + if (_controller is null) + { + return; + } + + CancellationToken cancellationToken = _lifetimeCancellation.Token; + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested || _shuttingDown) + { + return; + } + + ShadowsocksController? controller = _controller; + if (controller is null) + { + return; + } + + List failures = await controller.UpdateAllOnlineConfig().ConfigureAwait(false); + if (failures.Count > 0) + { + Logger.Warn("Startup online-config refresh completed with {0} failure(s): {1}", failures.Count, string.Join(", ", failures)); + } + else + { + Logger.Info("Startup online-config refresh completed."); + } + } + catch (OperationCanceledException) + { + // Normal during application shutdown. + } + catch (Exception exception) + { + Logger.Error(exception, "Startup online-config refresh failed."); + } + }, cancellationToken); + } + + private IReadOnlyList RegisterHotkeys(HotkeyConfig config) + { + if (_controller is null) + { + return new[] { _localization["Controller unavailable"] }; + } + + return _hotkeyManager?.ApplyConfiguration(config) ?? Array.Empty(); + } + + private IReadOnlyList ApplyHotkeys(HotkeyConfig config) + { + if (_controller is null) + { + return new[] { _localization["Controller unavailable"] }; + } + + _controller.SaveHotkeyConfig(config); + return _hotkeyManager?.ApplyConfiguration(config) ?? Array.Empty(); + } + + private void InitializeSystemShell() + { + if (_window is null || _dispatcherQueue is null) + { + throw new InvalidOperationException("The main WinUI window is not initialized."); + } + + _trayIcon = new TrayIconService( + RuntimeEnvironment.ExecutablePath, + BuildTrayMenuState(), + text => _localization[text]); + _trayIcon.CommandRequested += OnTrayCommandRequested; + + if (_controller is not null) + { + _hotkeyManager = new WinUIHotkeyManager( + _controller, + _dispatcherQueue, + ShowMainWindow, + _window.NavigateToLogs); + var hotkeyFailures = _hotkeyManager.RegisterConfiguredHotkeys(); + if (hotkeyFailures.Count > 0) + { + _window.SetShellStatus(_localization.Format("Tray active · {0} hotkey(s) failed to register", hotkeyFailures.Count), InfoBarSeverity.Warning); + } + else + { + _window.SetShellStatus(_localization["Single instance · tray active · system shell ready"]); + } + } + else + { + _window.SetShellStatus(_localization["Single instance · tray active · controller unavailable"], InfoBarSeverity.Warning); + } + } + + private void SubscribeControllerEvents(ShadowsocksController controller) + { + controller.ConfigChanged += OnControllerStateChanged; + controller.EnableStatusChanged += OnControllerStateChanged; + controller.EnableGlobalChanged += OnControllerStateChanged; + controller.ShareOverLANStatusChanged += OnControllerStateChanged; + controller.TrafficModeChanged += OnControllerStateChanged; + controller.DnsCryptStatusChanged += OnControllerStateChanged; + controller.TrafficChanged += OnControllerTrafficChanged; + controller.UserRuleFileReadyToOpen += OnControllerPathReadyToOpen; + controller.Errored += OnControllerErrored; + } + + private void UnsubscribeControllerEvents(ShadowsocksController controller) + { + controller.ConfigChanged -= OnControllerStateChanged; + controller.EnableStatusChanged -= OnControllerStateChanged; + controller.EnableGlobalChanged -= OnControllerStateChanged; + controller.ShareOverLANStatusChanged -= OnControllerStateChanged; + controller.TrafficModeChanged -= OnControllerStateChanged; + controller.DnsCryptStatusChanged -= OnControllerStateChanged; + controller.TrafficChanged -= OnControllerTrafficChanged; + controller.UserRuleFileReadyToOpen -= OnControllerPathReadyToOpen; + controller.Errored -= OnControllerErrored; + } + + private void OnMainWindowContentLoaded(object sender, RoutedEventArgs e) + { + if (sender is FrameworkElement contentRoot) + { + contentRoot.Loaded -= OnMainWindowContentLoaded; + } + + _ = DrainControllerErrorQueueAsync(); + } + + private void OnControllerErrored(object? _, System.IO.ErrorEventArgs e) + { + Exception exception = e.GetException(); + _pendingControllerErrors.Enqueue(exception); + DispatcherQueue? dispatcherQueue = _dispatcherQueue; + if (dispatcherQueue is not null) + { + _ = dispatcherQueue.TryEnqueue(() => + { + // Controller errors are surfaced immediately even when + // it was otherwise tray-only. Lazily activate the WinUI host only for + // that exceptional dialog path. + if (_window?.Content?.XamlRoot is null) + { + ShowMainWindow(); + } + + _ = DrainControllerErrorQueueAsync(); + }); + } + } + + private void OnControllerPathReadyToOpen(object? _, ShadowsocksController.PathEventArgs e) + { + if (string.IsNullOrWhiteSpace(e.Path)) + { + return; + } + + _ = _dispatcherQueue?.TryEnqueue(() => + { + try + { + Process.Start(new ProcessStartInfo(e.Path) { UseShellExecute = true }); + } + catch (Exception exception) + { + _window?.SetShellStatus(_localization.Format("Failed to open {0}: {1}", e.Path, exception.Message), InfoBarSeverity.Error); + } + }); + } + + private async Task DrainControllerErrorQueueAsync() + { + if (_controllerErrorDialogActive || _window is null || _window.Content?.XamlRoot is null) + { + return; + } + + _controllerErrorDialogActive = true; + try + { + while (_pendingControllerErrors.TryDequeue(out Exception? exception)) + { + try + { + await _window.ShowControllerErrorAsync(exception); + } + catch (Exception dialogException) + { + Logger.Error(dialogException, "Failed to display a controller error dialog."); + } + } + } + finally + { + _controllerErrorDialogActive = false; + } + } + + private void OnControllerStateChanged(object? _, EventArgs _1) + { + _ = _dispatcherQueue?.TryEnqueue(UpdateTrayState); + } + + private void OnControllerTrafficChanged(object? _, EventArgs _1) + { + if (_dispatcherQueue is null || _controller is null) + { + return; + } + + ShadowsocksController.TrafficPerSecond? current = _controller.GetLatestTrafficSample(); + bool inbound = current?.InboundIncrement > 0; + bool outbound = current?.OutboundIncrement > 0; + _ = _dispatcherQueue.TryEnqueue(() => _trayIcon?.UpdateActivity(inbound, outbound)); + } + + private void UpdateTrayState() + { + if (_trayIcon is not null) + { + _trayIcon.UpdateState(BuildTrayMenuState()); + } + } + + private TrayMenuState BuildTrayMenuState() + { + if (_controller is null) + { + return new TrayMenuState( + _localization["shadowsocks-reborn"] + "\n" + _localization["Controller unavailable"], + TraySystemProxyMode.Disabled, + TrayTrafficMode.User, + _localization["WinDivert: inactive"], + TrayDnsMode.System, + _localization["DNSCrypt: Not installed"], + false, + false, + Array.Empty(), + Array.Empty(), + false, false, false, false, + AutoStartup.Check(), + !AppStoragePaths.IsCleanMode, + ProtocolHandler.Check(), + false, + false, + 0); + } + + Configuration config = _controller.GetCurrentConfiguration(); + SystemProxyMode proxyMode = _controller.GetSystemProxyMode(); + TrafficCaptureStatus traffic = _controller.GetTrafficCaptureStatus(); + + TraySystemProxyMode trayProxyMode = proxyMode switch + { + SystemProxyMode.Global => TraySystemProxyMode.Global, + SystemProxyMode.Pac => TraySystemProxyMode.Pac, + _ => TraySystemProxyMode.Disabled, + }; + TrayTrafficMode trayTrafficMode = config.trafficCaptureMode == TrafficCaptureMode.Admin + ? TrayTrafficMode.Admin + : TrayTrafficMode.User; + string trafficStatus = traffic.RuntimeMode switch + { + TrafficRuntimeMode.Admin => _localization["WinDivert: active"], + TrafficRuntimeMode.Game => _localization["WinDivert: paused (game running)"], + _ => _localization["WinDivert: inactive"], + }; + + DnsCryptManagementStatus dns = _controller.GetDnsCryptManagementStatus(); + TrayDnsMode trayDnsMode = dns.Mode switch + { + DnsPolicyMode.Direct => TrayDnsMode.Direct, + DnsPolicyMode.Proxy => TrayDnsMode.Proxy, + DnsPolicyMode.CustomDoh => TrayDnsMode.CustomDoh, + DnsPolicyMode.DnsCrypt => TrayDnsMode.DnsCrypt, + _ => TrayDnsMode.System, + }; + DnsCryptTrayPresentation dnsPresentation = DnsCryptPresentation.GetTrayPresentation(dns); + string dnsStatus = dnsPresentation.Version is null + ? _localization[dnsPresentation.LocalizationKey] + : _localization.Format(dnsPresentation.LocalizationKey, dnsPresentation.Version); + + var strategies = _controller.GetStrategies() + .Select(strategy => new TrayStrategyMenuItem( + strategy.ID, + strategy.Name, + string.Equals(config.strategy, strategy.ID, StringComparison.Ordinal))) + .ToArray(); + + int configuredServerCount = config.configs.Count(server => server?.IsConfigured == true); + var serverIndices = Enumerable.Range(0, config.configs.Count) + .Where(index => config.configs[index]?.IsConfigured == true) + .Take(20) + .ToList(); + if (config.index >= 0 + && config.index < config.configs.Count + && config.configs[config.index]?.IsConfigured == true + && !serverIndices.Contains(config.index)) + { + serverIndices.Add(config.index); + } + var servers = serverIndices + .Select(index => new TrayServerMenuItem(index, config.configs[index].ToString(), string.IsNullOrEmpty(config.strategy) && config.index == index)) + .ToArray(); + + string serverInfo = config.HasConfiguredServer + ? (_controller.GetCurrentStrategy()?.Name ?? _controller.GetCurrentServer().ToString()) + : _localization["No server configured"]; + string proxyText = proxyMode == SystemProxyMode.Disabled + ? _localization.Format("Running: Port {0}", config.localPort) + : _localization["System Proxy On:"] + " " + (proxyMode == SystemProxyMode.Global ? _localization["Global"] : _localization["PAC"]); + string tooltip = $"{_localization["shadowsocks-reborn"]} {ApplicationInfo.Version}\n{proxyText}\n{serverInfo}"; + + return new TrayMenuState( + tooltip, + trayProxyMode, + trayTrafficMode, + trafficStatus, + trayDnsMode, + dnsStatus, + dns.Component.IsInstalled, + dns.UpdateAvailable, + strategies, + servers, + config.HasConfiguredServer, + config.useOnlinePac, + config.secureLocalPac, + config.regeneratePacOnUpdate, + AutoStartup.Check(), + !AppStoragePaths.IsCleanMode, + ProtocolHandler.Check(), + config.shareOverLan, + config.checkPreRelease, + configuredServerCount); + } + + private async void OnTrayCommandRequested(object? _, TrayCommandEventArgs e) + { + if (_window is null) + { + return; + } + + try + { + switch (e.Command) + { + case TrayCommandKind.OpenOverview: + _window.NavigateToOverview(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenServers: + _window.NavigateToServers(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenPlugins: + _window.NavigateToPlugins(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenTraffic: + _window.NavigateToTraffic(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenDns: + _window.NavigateToDns(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenSharing: + _window.NavigateToSharing(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenPac: + _window.NavigateToPac(); + ShowMainWindow(); + break; + case TrayCommandKind.EditOnlinePacUrl: + await EditOnlinePacUrlAsync(enableOnlineAfterSave: false); + break; + case TrayCommandKind.OpenForwardProxy: + _window.NavigateToForwardProxy(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenOnlineConfig: + _window.NavigateToOnlineConfig(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenHotkeys: + _window.NavigateToHotkeys(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenLogs: + _window.NavigateToLogs(); + ShowMainWindow(); + break; + case TrayCommandKind.OpenAbout: + _window.NavigateToAbout(); + ShowMainWindow(); + break; + case TrayCommandKind.CheckUpdates: + _window.NavigateToAbout(checkNow: true); + ShowMainWindow(); + break; + case TrayCommandKind.SetSystemProxyDisabled: + _controller?.ToggleEnable(false); + break; + case TrayCommandKind.SetSystemProxyPac: + if (_controller is not null) + { + _controller.ToggleEnable(true); + _controller.ToggleGlobal(false); + } + break; + case TrayCommandKind.SetSystemProxyGlobal: + if (_controller is not null) + { + _controller.ToggleEnable(true); + _controller.ToggleGlobal(true); + } + break; + case TrayCommandKind.SetTrafficUser: + if (_controller is not null) + { + await _controller.SetTrafficCaptureModeAsync(TrafficCaptureMode.User); + } + break; + case TrayCommandKind.SetTrafficAdmin: + if (_controller is not null) + { + await _controller.SetTrafficCaptureModeAsync(TrafficCaptureMode.Admin); + } + break; + case TrayCommandKind.SetDnsSystem: + if (_controller is not null) + { + await _controller.SetDnsPolicyAsync(DnsPolicyMode.System); + } + break; + case TrayCommandKind.SetDnsDirect: + if (_controller is not null) + { + await _controller.SetDnsPolicyAsync(DnsPolicyMode.Direct); + } + break; + case TrayCommandKind.SetDnsProxy: + if (_controller is not null) + { + await _controller.SetDnsPolicyAsync(DnsPolicyMode.Proxy); + } + break; + case TrayCommandKind.SetDnsDnsCrypt: + if (_controller is not null) + { + if (_controller.GetDnsCryptManagementStatus().Component.IsInstalled) + { + await _controller.SetDnsPolicyAsync(DnsPolicyMode.DnsCrypt); + } + else + { + ShowMainWindow(); + await _window.NavigateToDnsAndEnableAsync(); + } + } + break; + case TrayCommandKind.CheckDnsCryptUpdate: + if (_controller is not null) + { + DnsCryptReleaseInfo release = await _controller.CheckDnsCryptUpdateAsync(); + DnsCryptManagementStatus dns = _controller.GetDnsCryptManagementStatus(); + _window.SetShellStatus(dns.UpdateAvailable + ? _localization.Format("DNSCrypt Proxy {0} is available.", release.Version) + : _localization["DNSCrypt Proxy is up to date."]); + } + break; + case TrayCommandKind.SelectServer: + if (_controller is not null && e.Index >= 0) + { + _controller.SelectServerIndex(e.Index); + } + break; + case TrayCommandKind.SelectStrategy: + if (_controller is not null && !string.IsNullOrWhiteSpace(e.Value)) + { + _controller.SelectStrategy(e.Value); + } + break; + case TrayCommandKind.UseLocalPac: + _controller?.UseOnlinePAC(false); + break; + case TrayCommandKind.UseOnlinePac: + if (_controller is not null) + { + if (!IsValidPacUrl(_controller.GetCurrentConfiguration().pacUrl)) + { + await EditOnlinePacUrlAsync(enableOnlineAfterSave: true); + } + else + { + _controller.UseOnlinePAC(true); + } + } + break; + case TrayCommandKind.EditUserRuleFile: + _controller?.TouchUserRuleFile(); + break; + case TrayCommandKind.UpdateLocalPacFromGeosite: + if (_controller is not null) + { + bool updated = await _controller.UpdatePACFromGeositeAsync(); + _window.SetShellStatus( + updated ? _localization["Local PAC updated from GeoSite"] : _localization["GeoSite update did not complete"], + updated ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + break; + case TrayCommandKind.ToggleSecureLocalPac: + if (_controller is not null) + { + _controller.ToggleSecureLocalPac(!_controller.GetCurrentConfiguration().secureLocalPac); + } + break; + case TrayCommandKind.ToggleRegeneratePacOnUpdate: + if (_controller is not null) + { + _controller.ToggleRegeneratePacOnUpdate(!_controller.GetCurrentConfiguration().regeneratePacOnUpdate); + } + break; + case TrayCommandKind.ToggleStartWithWindows: + await SetStartWithWindowsAsync(!AutoStartup.Check()); + break; + case TrayCommandKind.ToggleProtocolHandler: + if (!ProtocolHandler.Set(!ProtocolHandler.Check())) + { + _window.SetShellStatus(_localization["Failed to update ss:// protocol association"], InfoBarSeverity.Error); + } + break; + case TrayCommandKind.ToggleAllowLan: + if (_controller is not null) + { + _controller.ToggleShareOverLAN(!_controller.GetCurrentConfiguration().shareOverLan); + } + break; + case TrayCommandKind.TogglePreRelease: + if (_controller is not null) + { + _controller.ToggleCheckingPreRelease(!_controller.GetCurrentConfiguration().checkPreRelease); + } + break; + case TrayCommandKind.Exit: + ShutdownAndExit(); + return; + } + } + catch (Exception exception) + { + Logger.Error(exception, "Tray command failed: {0}", e.Command); + _window.SetShellStatus(_localization.Format("Tray command failed: {0}", exception.Message), InfoBarSeverity.Error); + } + finally + { + UpdateTrayState(); + } + } + + private async Task EditOnlinePacUrlAsync(bool enableOnlineAfterSave) + { + if (_controller is null || _window is null) + { + return; + } + + // For Online PAC, if no URL exists, editing is + // performed first and Online PAC is enabled only after a non-empty URL is saved. + ShowMainWindow(); + await Task.Yield(); + + XamlRoot? xamlRoot = _window.Content?.XamlRoot; + if (xamlRoot is null) + { + _window.NavigateToPac(); + _window.SetShellStatus(_localization["Please input PAC Url"], InfoBarSeverity.Warning); + return; + } + + string originalUrl = _controller.GetCurrentConfiguration().pacUrl ?? string.Empty; + var input = new TextBox + { + Text = originalUrl, + MinWidth = 480, + PlaceholderText = "https://example.com/proxy.pac", + SelectionStart = originalUrl.Length, + }; + string pacUrlTooltip = _localization["Enter an absolute HTTP or HTTPS PAC URL."]; + ToolTipService.SetToolTip(input, pacUrlTooltip); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetHelpText(input, pacUrlTooltip); + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _localization["Edit Online PAC URL"], + Content = input, + PrimaryButtonText = _localization["OK"], + CloseButtonText = _localization["Cancel"], + DefaultButton = ContentDialogButton.Primary, + }; + + ContentDialogResult result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) + { + return; + } + + string pacUrl = input.Text.Trim(); + if (!IsValidPacUrl(pacUrl)) + { + _window.SetShellStatus(_localization["Enter an absolute HTTP or HTTPS PAC URL."], InfoBarSeverity.Warning); + return; + } + + if (!string.Equals(pacUrl, originalUrl, StringComparison.Ordinal)) + { + _controller.SavePACUrl(pacUrl); + } + + if (enableOnlineAfterSave) + { + _controller.UseOnlinePAC(true); + } + } + + private void StartAutomaticUpdateCheck() + { + if (_controller is null || _startupUpdateChecker is not null) + { + return; + } + + _startupUpdateChecker = new UpdateChecker(_controller); + _startupUpdateChecker.UpdateAvailable += OnAutomaticUpdateAvailable; + _ = _startupUpdateChecker.CheckForVersionUpdate(3000); + } + + private void OnAutomaticUpdateAvailable(object? _, UpdateAvailableEventArgs e) + { + _ = _dispatcherQueue?.TryEnqueue(async () => + { + UpdateChecker? checker = _startupUpdateChecker; + if (_window is null || checker is null || _shuttingDown) + { + return; + } + + try + { + _window.SetShellStatus(_localization.Format("Installing update {0}…", checker.NewReleaseVersion)); + if (await checker.DoUpdate()) + { + ShutdownAndExit(); + } + } + catch (Exception exception) + { + Logger.Error(exception, "Automatic application update failed."); + _window?.SetShellStatus( + _localization.Format("Automatic update failed: {0}", exception.Message), + InfoBarSeverity.Error); + } + }); + } + + private void OnRedirectedActivation(AppActivationArguments activationArguments) + { + DispatcherQueue? dispatcherQueue = _dispatcherQueue; + if (dispatcherQueue is null) + { + return; + } + + if (Program.IsPlainLaunch(activationArguments)) + { + _ = dispatcherQueue.TryEnqueue(async () => await ShowAlreadyRunningDialogAsync()); + return; + } + + _ = dispatcherQueue.TryEnqueue(() => ProcessActivation(activationArguments, showWindow: false)); + } + + private async Task ShowAlreadyRunningDialogAsync() + { + MainWindow? window = _window; + if (window is null || _alreadyRunningDialogActive) + { + return; + } + + _alreadyRunningDialogActive = true; + try + { + window.ShowFromTray(); + await window.ShowAlreadyRunningDialogAsync(); + } + catch (InvalidOperationException exception) + { + // ContentDialog allows one modal dialog per XamlRoot. If another one is + // already open, keep the activation useful and fall back to the Fluent + // shell InfoBar instead of ever returning to a Win32 MessageBox. + Logger.Debug(exception, "The already-running ContentDialog could not be shown because another dialog is active."); + window.SetShellStatus(_localization["Shadowsocks Reborn is already running"], InfoBarSeverity.Informational); + } + finally + { + _alreadyRunningDialogActive = false; + } + } + + private void ProcessActivation(AppActivationArguments activationArguments, bool showWindow) + { + if (showWindow) + { + ShowMainWindow(); + } + + if (_controller is null) + { + return; + } + + string? openUrl = TryGetOpenUrl(activationArguments); + if (!string.IsNullOrWhiteSpace(openUrl) && _window is not null) + { + ShowMainWindow(); + _ = _window.NavigateToSharingAndImportAsync(openUrl); + } + } + + private static string? TryGetOpenUrl(AppActivationArguments activationArguments) + { + if (activationArguments.Kind == ExtendedActivationKind.Protocol + && activationArguments.Data is IProtocolActivatedEventArgs protocolArguments) + { + return protocolArguments.Uri?.AbsoluteUri; + } + + if (activationArguments.Kind != ExtendedActivationKind.Launch + || activationArguments.Data is not ILaunchActivatedEventArgs launchArguments) + { + return null; + } + + string[] arguments = WindowsCommandLine.ParseArguments(launchArguments.Arguments); + for (int index = 0; index < arguments.Length; index++) + { + string argument = arguments[index]; + if (argument.StartsWith("--open-url=", StringComparison.OrdinalIgnoreCase)) + { + return argument["--open-url=".Length..].Trim('"'); + } + + if (argument.Equals("--open-url", StringComparison.OrdinalIgnoreCase) + && index + 1 < arguments.Length) + { + return arguments[index + 1].Trim('"'); + } + } + + return null; + } + + private static bool IsValidPacUrl(string? value) + => Uri.TryCreate(value?.Trim(), UriKind.Absolute, out Uri? uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); + + private async Task SetStartWithWindowsAsync(bool enabled) + { + if (AppStoragePaths.IsCleanMode) + { + _window?.SetShellStatus(_localization["Start on Boot is unavailable in Clean Mode."], InfoBarSeverity.Warning); + return false; + } + + bool windowVisible = _window?.IsVisibleToUser == true; + bool updated = await Task.Run(() => AutoStartup.Set(enabled, windowVisible)); + if (!updated) + { + return false; + } + + UpdateTrayState(); + _window?.SetShellStatus( + enabled ? _localization["Start on Boot enabled"] : _localization["Start on Boot disabled"], + InfoBarSeverity.Success); + return true; + } + + private void OnWindowVisibilityChanged(bool visible) + { + if (_systemSessionEnding || _shuttingDown || AppStoragePaths.IsCleanMode) + { + return; + } + + AutoStartup.SynchronizeUiState(visible); + } + + private void ShowMainWindow() + { + _window?.ShowFromTray(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + Program.UnregisterActivationHandler(OnRedirectedActivation); + Program.DetachActivationSource(_appInstance); + + _lifetimeCancellation.Cancel(); + _resumeRestartCancellation?.Cancel(); + _resumeRestartCancellation?.Dispose(); + _resumeRestartCancellation = null; + + if (_powerModeMonitor is not null) + { + _powerModeMonitor.SessionEnding -= OnSystemSessionEnding; + _powerModeMonitor.SessionEndCancelled -= OnSystemSessionEndCancelled; + _powerModeMonitor.Suspending -= OnSystemSuspending; + _powerModeMonitor.Resumed -= OnSystemResumed; + _powerModeMonitor.Dispose(); + _powerModeMonitor = null; + } + + if (_trayIcon is not null) + { + _trayIcon.CommandRequested -= OnTrayCommandRequested; + _trayIcon.Dispose(); + _trayIcon = null; + } + + _hotkeyManager?.Dispose(); + _hotkeyManager = null; + + if (_startupUpdateChecker is not null) + { + _startupUpdateChecker.UpdateAvailable -= OnAutomaticUpdateAvailable; + _startupUpdateChecker = null; + } + + if (!AppStoragePaths.IsCleanMode && !_systemSessionEnding) + { + // A deliberate application exit must cancel Restart Manager restoration. + // During Windows shutdown/logoff we keep the registration intact so Windows + // can restore the state captured by WM_QUERYENDSESSION. + AutoStartup.RegisterForRestart(false); + } + + _window?.DisposeCachedPages(); + + if (_controller is not null) + { + UnsubscribeControllerEvents(_controller); + try + { + _controller.Dispose(); + } + catch (Exception exception) + { + Logger.Error(exception, "Controller shutdown failed."); + } + finally + { + _controller = null; + } + } + + _userInteraction = null; + _lifetimeCancellation.Dispose(); + GC.SuppressFinalize(this); + } + + private void ShutdownAndExit() + { + if (_shuttingDown) + { + return; + } + + _shuttingDown = true; + Dispose(); + _appInstance.UnregisterKey(); + + MainWindow? window = _window; + _window = null; + window?.CloseForApplicationExit(); + + if (AppStoragePaths.IsCleanMode) + { + LogManager.Shutdown(); + AppStoragePaths.CleanupCleanSession(); + } + + Exit(); + } + + private static void OnUnhandledException(object _, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + Logger.Error(e.Exception, "Unhandled WinUI system-shell exception."); + } +} diff --git a/Shadowsocks.WinUI/App.xaml b/Shadowsocks.WinUI/App.xaml new file mode 100644 index 000000000..9cc21833f --- /dev/null +++ b/Shadowsocks.WinUI/App.xaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/Shadowsocks.WinUI/MainWindow.cs b/Shadowsocks.WinUI/MainWindow.cs new file mode 100644 index 000000000..8e55bb68e --- /dev/null +++ b/Shadowsocks.WinUI/MainWindow.cs @@ -0,0 +1,792 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Shadowsocks.Controller; +using Shadowsocks.Model; +using Shadowsocks.Localization; +using Shadowsocks.WinUI.Pages; +using Shadowsocks.WinUI.UI; +using Windows.Graphics; + +namespace Shadowsocks.WinUI; + +public sealed class MainWindow : Window +{ + private const string OverviewTag = "overview"; + private const string ServersTag = "servers"; + private const string PluginsTag = "plugins"; + private const string TrafficTag = "traffic"; + private const string DnsTag = "dns"; + private const string GamesTag = "games"; + private const string PacTag = "pac"; + private const string ForwardProxyTag = "forward-proxy"; + private const string OnlineConfigTag = "online-config"; + private const string HotkeysTag = "hotkeys"; + private const string SharingTag = "sharing"; + private const string LogsTag = "logs"; + private const string AboutTag = "about"; + + private readonly Grid _rootGrid; + private readonly TitleBar _titleBar; + private readonly NavigationView _navigationView; + private readonly ContentControl _contentHost; + private readonly InfoBar _statusInfoBar; + private readonly ShadowsocksController? _controller; + private readonly Func _getStartWithWindows; + private readonly Func> _setStartWithWindows; + private readonly WinUIPageContext _pageContext; + private readonly Dictionary _pageCache = new(); + private readonly ILocalizationService _localization; + private readonly Action _visibilityChanged; + + private AppThemePreference _themePreference; + private Type? _currentPageType; + private bool _hasBeenActivated; + private bool _isVisibleToUser; + private bool _systemSessionEnding; + private bool _applicationExitRequested; + + public MainWindow( + ShadowsocksController? controller, + Func getStartWithWindows, + Func> setStartWithWindows, + Func> registerHotkeys, + Func> applyHotkeys, + Action requestApplicationExit, + Action visibilityChanged, + ILocalizationService localization) + { + _controller = controller; + _getStartWithWindows = getStartWithWindows; + _setStartWithWindows = setStartWithWindows; + ArgumentNullException.ThrowIfNull(visibilityChanged); + _visibilityChanged = visibilityChanged; + ArgumentNullException.ThrowIfNull(localization); + _localization = localization; + _themePreference = ParseTheme(controller?.GetCurrentConfiguration().uiTheme); + + Title = _localization["Shadowsocks Reborn"]; + AppWindow.Resize(new SizeInt32(1180, 760)); + AppWindow.Closing += OnAppWindowClosing; + SystemBackdrop = new MicaBackdrop { Kind = MicaKind.Base }; + + _rootGrid = new Grid(); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + + _titleBar = CreateTitleBar(); + Grid.SetRow(_titleBar, 0); + _rootGrid.Children.Add(_titleBar); + + _navigationView = CreateNavigationView(); + Grid.SetRow(_navigationView, 1); + _rootGrid.Children.Add(_navigationView); + + _statusInfoBar = new InfoBar + { + IsOpen = false, + IsClosable = true, + Margin = new Thickness(WinUIStyles.PageGutter, 12, WinUIStyles.PageGutter, 0), + }; + + _contentHost = new ContentControl + { + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch, + Padding = new Thickness(WinUIStyles.PageGutter, 0, WinUIStyles.PageGutter, WinUIStyles.PageGutter), + }; + + var contentHost = new Grid(); + contentHost.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + contentHost.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + contentHost.Children.Add(_statusInfoBar); + Grid.SetRow(_contentHost, 1); + contentHost.Children.Add(_contentHost); + _navigationView.Content = contentHost; + + _pageContext = new WinUIPageContext( + _controller, + () => _rootGrid.XamlRoot, + () => WinRT.Interop.WindowNative.GetWindowHandle(this), + NavigateToSharing, + ShowInfo, + ShowDialogAsync, + () => _themePreference, + ApplyAndSaveTheme, + _getStartWithWindows, + _setStartWithWindows, + SetAlwaysOnTop, + registerHotkeys, + applyHotkeys, + requestApplicationExit, + _localization); + + Content = _rootGrid; + ExtendsContentIntoTitleBar = true; + SetTitleBar(_titleBar); + ApplyTheme(_themePreference); + + if (_controller is not null) + { + SubscribeControllerEvents(_controller); + } + + NavigateTo(OverviewTag); + _navigationView.SelectedItem = _navigationView.MenuItems[0]; + RefreshShellSummary(); + + } + + public void NavigateToLogs() + { + NavigateTo(LogsTag); + foreach (object item in _navigationView.MenuItems) + { + if (item is NavigationViewItem navigationItem + && string.Equals(navigationItem.Tag?.ToString(), LogsTag, StringComparison.Ordinal)) + { + _navigationView.SelectedItem = navigationItem; + break; + } + } + ShowFromTray(); + } + + public void NavigateToOverview() => NavigateAndSelect(OverviewTag); + public void NavigateToServers() => NavigateAndSelect(ServersTag); + public void NavigateToPlugins() => NavigateAndSelect(PluginsTag); + public void NavigateToTraffic() => NavigateAndSelect(TrafficTag); + public void NavigateToDns() + { + if (EnsureConfiguredServerForFeature("DNS")) + NavigateAndSelect(DnsTag); + } + + public async Task NavigateToDnsAndEnableAsync() + { + if (!EnsureConfiguredServerForFeature("DNS")) + return; + + NavigateAndSelect(DnsTag); + ShowFromTray(); + if (_contentHost.Content is DnsPage dnsPage) + await dnsPage.RequestEnableDnsCryptAsync(); + } + public void NavigateToPac() + { + if (EnsureConfiguredServerForFeature("PAC / GeoSite")) + NavigateAndSelect(PacTag); + } + public void NavigateToForwardProxy() => NavigateAndSelect(ForwardProxyTag); + public void NavigateToHotkeys() => NavigateAndSelect(HotkeysTag); + public void NavigateToSharing() => NavigateAndSelect(SharingTag); + + public void NavigateToSharing(int serverIndex) + { + NavigateAndSelect(SharingTag); + if (_contentHost.Content is SharingPage sharingPage) + { + sharingPage.SelectServer(serverIndex); + } + } + + public void NavigateToAbout(bool checkNow = false) + { + NavigateAndSelect(AboutTag); + if (checkNow && _contentHost.Content is AboutPage aboutPage) + { + _ = aboutPage.CheckForUpdatesAsync(); + } + } + + public void NavigateToOnlineConfig() => NavigateAndSelect(OnlineConfigTag); + + public async Task NavigateToSharingAndImportAsync(string url) + { + NavigateAndSelect(SharingTag); + ShowFromTray(); + if (_contentHost.Content is SharingPage sharingPage) + { + await sharingPage.ImportUrlAsync(url); + } + } + + public bool IsVisibleToUser => _isVisibleToUser; + + public void HideToTray() + { + if (_applicationExitRequested) + { + return; + } + + AppWindow.Hide(); + SetVisibleToUser(false); + } + + public void ShowFromTray() + { + if (_applicationExitRequested) + { + return; + } + + if (!_hasBeenActivated) + { + _hasBeenActivated = true; + Activate(); + } + else + { + if (AppWindow.Presenter is OverlappedPresenter presenter + && presenter.State == OverlappedPresenterState.Minimized) + { + presenter.Restore(true); + } + + AppWindow.Show(true); + Activate(); + } + + SetVisibleToUser(true); + + // AppWindow.Show(true) requests activation, but an already-visible desktop + // window may still remain behind another top-level window. A tray click is + // explicit user interaction, so also request foreground activation for the HWND. + nint windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(this); + if (windowHandle != nint.Zero) + { + _ = SetForegroundWindow(windowHandle); + } + } + + private void SetVisibleToUser(bool visible) + { + if (_isVisibleToUser == visible) + { + return; + } + + _isVisibleToUser = visible; + _visibilityChanged(visible); + } + + public void PrepareForSystemSessionEnd() + { + _systemSessionEnding = true; + } + + public void CancelSystemSessionEnd() + { + _systemSessionEnding = false; + } + + public void SetShellStatus(string status, InfoBarSeverity severity) + { + if (string.IsNullOrWhiteSpace(status)) + { + return; + } + + ShowInfo(_localization["System shell"], status, severity); + } + + public void SetShellStatus(string status) + { + if (string.IsNullOrWhiteSpace(status)) + { + return; + } + + // Severity must never depend on English keywords after the message has been localized. + // Callers that need Warning/Error use the explicit overload; the default is informational. + ShowInfo(_localization["System shell"], status, InfoBarSeverity.Informational); + } + + + public Task ShowMessageAsync(string title, string content, string closeButtonText = "OK") + => ShowDialogAsync(_localization[title], _localization[content], _localization[closeButtonText]); + + public async Task ShowAlreadyRunningDialogAsync() + { + XamlRoot? xamlRoot = await GetDialogXamlRootAsync(); + if (xamlRoot is null) + { + SetShellStatus(_localization["Shadowsocks Reborn is already running"], InfoBarSeverity.Informational); + return; + } + + var icon = new FontIcon + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uE946", + FontSize = 30, + Margin = new Thickness(2, 2, 16, 0), + VerticalAlignment = VerticalAlignment.Top, + }; + + var text = new StackPanel + { + Spacing = 8, + MinWidth = 300, + MaxWidth = 480, + }; + text.Children.Add(new TextBlock + { + Text = _localization["The existing instance has been brought to the foreground."], + TextWrapping = TextWrapping.Wrap, + }); + text.Children.Add(new TextBlock + { + Text = _localization["Closing the window hides it to the notification area. The proxy controller keeps running until you choose Exit from the tray menu."], + TextWrapping = TextWrapping.Wrap, + Opacity = 0.72, + }); + + var content = new Grid + { + Margin = new Thickness(0, 4, 0, 0), + }; + content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + content.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + content.Children.Add(icon); + Grid.SetColumn(text, 1); + content.Children.Add(text); + + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _localization["Shadowsocks Reborn is already running"], + Content = content, + CloseButtonText = _localization["OK"], + DefaultButton = ContentDialogButton.Close, + }; + + await dialog.ShowAsync(); + } + + private async Task GetDialogXamlRootAsync() + { + XamlRoot? xamlRoot = _rootGrid.XamlRoot; + if (xamlRoot is not null) + { + return xamlRoot; + } + + var loaded = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + RoutedEventHandler handler = null!; + handler = (_, _) => + { + _rootGrid.Loaded -= handler; + loaded.TrySetResult(true); + }; + _rootGrid.Loaded += handler; + + try + { + _ = await Task.WhenAny(loaded.Task, Task.Delay(TimeSpan.FromSeconds(2))); + return _rootGrid.XamlRoot; + } + finally + { + _rootGrid.Loaded -= handler; + } + } + + public async Task ShowControllerErrorAsync(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + XamlRoot? xamlRoot = _rootGrid.XamlRoot; + if (xamlRoot is null) + { + return; + } + + var details = new TextBox + { + Text = exception.ToString(), + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + MinWidth = 520, + MaxWidth = 720, + MaxHeight = 360, + }; + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _localization.Format("shadowsocks-reborn Error: {0}", exception.Message), + Content = details, + CloseButtonText = _localization["OK"], + }; + await dialog.ShowAsync(); + } + + internal void DisposeCachedPages() + { + _contentHost.Content = null; + foreach (Page page in _pageCache.Values) + { + if (page is IDisposable disposablePage) + disposablePage.Dispose(); + } + _pageCache.Clear(); + _currentPageType = null; + } + + public void CloseForApplicationExit() + { + if (_applicationExitRequested) + { + return; + } + + _applicationExitRequested = true; + DisposeCachedPages(); + + if (_controller is not null) + { + UnsubscribeControllerEvents(_controller); + } + + Close(); + } + + private TitleBar CreateTitleBar() + { + var titleBar = new TitleBar + { + Title = _localization["Shadowsocks Reborn"], + Subtitle = _localization["Starting…"], + IconSource = new FontIconSource + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uE774", + }, + IsPaneToggleButtonVisible = true, + }; + titleBar.PaneToggleRequested += (_, _) => _navigationView.IsPaneOpen = !_navigationView.IsPaneOpen; + return titleBar; + } + + private NavigationView CreateNavigationView() + { + var navigationView = new NavigationView + { + PaneDisplayMode = NavigationViewPaneDisplayMode.Left, + IsBackButtonVisible = NavigationViewBackButtonVisible.Collapsed, + IsPaneToggleButtonVisible = false, + IsTitleBarAutoPaddingEnabled = false, + AlwaysShowHeader = false, + IsSettingsVisible = true, + OpenPaneLength = 272, + CompactPaneLength = 48, + }; + + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Overview"], OverviewTag, "\uE80F", "Open the connection overview and current runtime status.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Servers"], ServersTag, "\uE909", "Manage Shadowsocks servers and local client connection settings.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Plugins"], PluginsTag, "\uE710", "Install and manage SIP003 plugins.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Traffic"], TrafficTag, "\uE895", "Configure capture mode, Windows proxy settings and per-application routing.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["DNS"], DnsTag, "\uE774", "Manage DNS policy and DNSCrypt Proxy.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Game Mode"], GamesTag, "\uE768", "Manage applications that automatically suspend WinDivert while they are running.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["PAC / GeoSite"], PacTag, "\uE774", "Configure PAC behavior, local PAC security and GeoSite sources.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Forward Proxy"], ForwardProxyTag, "\uE72A", "Configure an optional upstream proxy used to reach Shadowsocks servers.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Online Config"], OnlineConfigTag, "\uE895", "Manage SIP008 and other online server configuration sources.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Hotkeys"], HotkeysTag, "\uE765", "Configure global keyboard shortcuts for common Shadowsocks actions.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Share / QR"], SharingTag, "\uE72D", "Share servers and import ss:// links or QR codes.")); + navigationView.MenuItems.Add(new NavigationViewItemSeparator()); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["Logs"], LogsTag, "\uE8A5", "View logs, traffic history and logging options.")); + navigationView.MenuItems.Add(CreateNavigationItem(_localization["About"], AboutTag, "\uE897", "View version information and configure update checks.")); + + if (navigationView.SettingsItem is NavigationViewItem settingsItem) + { + settingsItem.Content = _localization["Settings"]; + settingsItem.Icon = WinUIStyles.CreateWindows10Icon("\uE713"); + string settingsTooltip = _localization["Open application appearance, startup and storage settings."]; + ToolTipService.SetToolTip(settingsItem, settingsTooltip); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetHelpText(settingsItem, settingsTooltip); + } + + navigationView.SelectionChanged += OnNavigationSelectionChanged; + navigationView.DisplayModeChanged += OnNavigationDisplayModeChanged; + return navigationView; + } + + private NavigationViewItem CreateNavigationItem(string content, string tag, string glyph, string tooltipKey) + { + var item = new NavigationViewItem + { + Content = content, + Tag = tag, + Icon = WinUIStyles.CreateWindows10Icon(glyph), + }; + string tooltip = _localization[tooltipKey]; + ToolTipService.SetToolTip(item, tooltip); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetHelpText(item, tooltip); + return item; + } + + private void OnNavigationSelectionChanged(NavigationView _, NavigationViewSelectionChangedEventArgs args) + { + if (args.IsSettingsSelected) + { + NavigatePage(typeof(SettingsPage)); + return; + } + + if (args.SelectedItemContainer?.Tag is string tag) + { + NavigateTo(tag); + } + } + + private void NavigateTo(string tag) + { + Type pageType = tag switch + { + ServersTag => typeof(ServersPage), + PluginsTag => typeof(PluginsPage), + TrafficTag => typeof(TrafficPage), + DnsTag => typeof(DnsPage), + GamesTag => typeof(GamesPage), + PacTag => typeof(PacGeositePage), + ForwardProxyTag => typeof(ForwardProxyPage), + OnlineConfigTag => typeof(OnlineConfigPage), + HotkeysTag => typeof(HotkeysPage), + SharingTag => typeof(SharingPage), + LogsTag => typeof(LogsPage), + AboutTag => typeof(AboutPage), + _ => typeof(OverviewPage), + }; + NavigatePage(pageType); + } + + private void NavigatePage(Type pageType) + { + if (_currentPageType == pageType) + { + (_contentHost.Content as IRefreshablePage)?.Refresh(); + return; + } + + Page page = CreatePage(pageType); + _currentPageType = pageType; + _contentHost.Content = page; + (page as IRefreshablePage)?.Refresh(); + } + + private Page CreatePage(Type pageType) + { + if (_pageCache.TryGetValue(pageType, out Page? cachedPage)) + { + return cachedPage; + } + + Page page = pageType == typeof(OverviewPage) ? new OverviewPage(_pageContext) + : pageType == typeof(ServersPage) ? new ServersPage(_pageContext) + : pageType == typeof(PluginsPage) ? new PluginsPage(_pageContext) + : pageType == typeof(TrafficPage) ? new TrafficPage(_pageContext) + : pageType == typeof(DnsPage) ? new DnsPage(_pageContext) + : pageType == typeof(GamesPage) ? new GamesPage(_pageContext) + : pageType == typeof(PacGeositePage) ? new PacGeositePage(_pageContext) + : pageType == typeof(ForwardProxyPage) ? new ForwardProxyPage(_pageContext) + : pageType == typeof(HotkeysPage) ? new HotkeysPage(_pageContext) + : pageType == typeof(SharingPage) ? new SharingPage(_pageContext) + : pageType == typeof(OnlineConfigPage) ? new OnlineConfigPage(_pageContext) + : pageType == typeof(LogsPage) ? new LogsPage(_pageContext) + : pageType == typeof(AboutPage) ? new AboutPage(_pageContext) + : pageType == typeof(SettingsPage) ? new SettingsPage(_pageContext) + : throw new ArgumentOutOfRangeException(nameof(pageType), pageType, "Unsupported WinUI page type."); + + _pageCache.Add(pageType, page); + return page; + } + + private void NavigateAndSelect(string tag) + { + NavigateTo(tag); + foreach (object item in _navigationView.MenuItems) + { + if (item is NavigationViewItem navigationItem + && string.Equals(navigationItem.Tag?.ToString(), tag, StringComparison.Ordinal)) + { + _navigationView.SelectedItem = navigationItem; + break; + } + } + } + + private bool EnsureConfiguredServerForFeature(string featureName) + { + if (_controller?.GetCurrentConfiguration().HasConfiguredServer == true) + return true; + + NavigateAndSelect(ServersTag); + ShowInfo( + _localization[featureName], + _localization["Add a configured Shadowsocks server before using DNS or PAC settings."], + InfoBarSeverity.Warning); + return false; + } + + private void UpdateServerDependentNavigationState() + { + bool available = _controller?.GetCurrentConfiguration().HasConfiguredServer == true; + foreach (object item in _navigationView.MenuItems) + { + if (item is not NavigationViewItem navigationItem) + continue; + + string? tag = navigationItem.Tag?.ToString(); + if (string.Equals(tag, DnsTag, StringComparison.Ordinal) + || string.Equals(tag, PacTag, StringComparison.Ordinal)) + { + navigationItem.IsEnabled = available; + } + } + + if (!available && (_currentPageType == typeof(DnsPage) || _currentPageType == typeof(PacGeositePage))) + { + NavigateAndSelect(ServersTag); + ShowInfo( + _localization["Servers"], + _localization["Add a configured Shadowsocks server before using DNS or PAC settings."], + InfoBarSeverity.Warning); + } + } + + private void OnNavigationDisplayModeChanged(NavigationView _, NavigationViewDisplayModeChangedEventArgs _1) + { + // Keep the page edge fixed when NavigationView changes between expanded, compact + // and minimal modes. Page-specific MaxWidth values used to make this edge jump. + double gutter = WinUIStyles.PageGutter; + _contentHost.Padding = new Thickness(gutter, 0, gutter, gutter); + _statusInfoBar.Margin = new Thickness(gutter, 12, gutter, 0); + } + + private void SubscribeControllerEvents(ShadowsocksController controller) + { + controller.ConfigChanged += OnControllerStateChanged; + controller.EnableStatusChanged += OnControllerStateChanged; + controller.EnableGlobalChanged += OnControllerStateChanged; + controller.ShareOverLANStatusChanged += OnControllerStateChanged; + controller.TrafficModeChanged += OnControllerStateChanged; + controller.DnsCryptStatusChanged += OnControllerStateChanged; + } + + private void UnsubscribeControllerEvents(ShadowsocksController controller) + { + controller.ConfigChanged -= OnControllerStateChanged; + controller.EnableStatusChanged -= OnControllerStateChanged; + controller.EnableGlobalChanged -= OnControllerStateChanged; + controller.ShareOverLANStatusChanged -= OnControllerStateChanged; + controller.TrafficModeChanged -= OnControllerStateChanged; + controller.DnsCryptStatusChanged -= OnControllerStateChanged; + } + + private void OnControllerStateChanged(object? _, EventArgs _1) + { + _ = _rootGrid.DispatcherQueue.TryEnqueue(() => + { + RefreshShellSummary(); + (_contentHost.Content as IRefreshablePage)?.Refresh(); + }); + } + + private void SetAlwaysOnTop(bool enabled) + { + if (AppWindow.Presenter is OverlappedPresenter presenter) + { + presenter.IsAlwaysOnTop = enabled; + } + } + + private void RefreshShellSummary() + { + UpdateServerDependentNavigationState(); + if (_controller is null) + { + _titleBar.Subtitle = _localization["Controller unavailable"]; + return; + } + + string running = _controller.IsProxyListenerRunning ? _localization["Running"] : _localization["Stopped"]; + _titleBar.Subtitle = _localization.Format("{0} · {1} mode", running, _localization[_controller.GetTrafficRuntimeMode().ToString()]); + } + + private void ApplyAndSaveTheme(AppThemePreference theme) + { + _themePreference = theme; + ApplyTheme(theme); + + _controller?.SetUiTheme(theme.ToString()); + + ShowInfo(_localization["Appearance"], theme == AppThemePreference.System + ? _localization["Theme now follows Windows."] + : _localization.Format("{0} theme enabled.", _localization[theme.ToString()]), InfoBarSeverity.Success); + } + + private void ApplyTheme(AppThemePreference theme) + { + _rootGrid.RequestedTheme = theme switch + { + AppThemePreference.Light => ElementTheme.Light, + AppThemePreference.Dark => ElementTheme.Dark, + _ => ElementTheme.Default, + }; + } + + [DllImport("user32.dll", SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(nint windowHandle); + + private static AppThemePreference ParseTheme(string? value) + { + return Enum.TryParse(value, ignoreCase: true, out AppThemePreference theme) + ? theme + : AppThemePreference.System; + } + + private void ShowInfo(string title, string message, InfoBarSeverity severity) + { + _statusInfoBar.Title = title; + _statusInfoBar.Message = message; + _statusInfoBar.Severity = severity; + _statusInfoBar.IsOpen = true; + } + + private async Task ShowDialogAsync(string title, string content, string closeButtonText) + { + XamlRoot? xamlRoot = _rootGrid.XamlRoot; + if (xamlRoot is null) + { + return ContentDialogResult.None; + } + + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = title, + Content = content, + CloseButtonText = closeButtonText, + }; + return await dialog.ShowAsync(); + } + + private void OnAppWindowClosing(AppWindow _, AppWindowClosingEventArgs args) + { + if (_applicationExitRequested || _systemSessionEnding) + { + return; + } + + args.Cancel = true; + HideToTray(); + } +} diff --git a/Shadowsocks.WinUI/Pages/AboutPage.cs b/Shadowsocks.WinUI/Pages/AboutPage.cs new file mode 100644 index 000000000..ac8c200ed --- /dev/null +++ b/Shadowsocks.WinUI/Pages/AboutPage.cs @@ -0,0 +1,388 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller; +using Shadowsocks.Core; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class AboutPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly UpdateChecker? _updateChecker; + private readonly TextBlock _updateStatus; + private readonly Button _checkButton; + private readonly Button _downloadButton; + private readonly Button _skipButton; + private readonly Button _notNowButton; + private readonly TextBlock _releaseHeading; + private readonly TextBox _releaseNotes; + private readonly ToggleSwitch _preReleaseToggle; + private readonly ToggleSwitch _updateAtStartupToggle; + private readonly ProgressRing _updateProgress; + private bool _refreshing; + private bool _updateFound; + + internal AboutPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + if (context.Controller is not null) + { + _updateChecker = new UpdateChecker(context.Controller); + _updateChecker.UpdateAvailable += OnUpdateAvailable; + _updateChecker.CheckUpdateCompleted += OnUpdateCheckCompleted; + } + + Content = WinUIStyles.CreatePage( + "About & updates", + "Version information, project links and update controls.", + out StackPanel panel); + + var identity = new StackPanel { Spacing = 10 }; + identity.Children.Add(WinUIStyles.CreateSectionTitle("Shadowsocks Reborn")); + identity.Children.Add(WinUIStyles.CreateText(_context.LF("Version {0}", ApplicationInfo.Version), "BodyStrongTextBlockStyle")); + identity.Children.Add(WinUIStyles.CreateText(".NET 10 · WinUI 3 · Windows App SDK · x64")); + identity.Children.Add(WinUIStyles.CreateText( + "Core networking is UI-independent. Windows integration, WinDivert coordination and tray support live in the Windows platform layer.")); + + var links = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + var projectButton = new Button { Content = "Project" }; + _context.SetToolTip(projectButton, "Open the Shadowsocks Reborn project page in your default browser."); + projectButton.Click += (_, _) => OpenUrl(ApplicationInfo.RepositoryUrl); + links.Children.Add(projectButton); + var issuesButton = new Button { Content = "Report issue" }; + _context.SetToolTip(issuesButton, "Open the GitHub issue tracker for bug reports and feature requests."); + issuesButton.Click += (_, _) => OpenUrl(ApplicationInfo.IssuesUrl); + links.Children.Add(issuesButton); + var licenseButton = new Button { Content = "License" }; + _context.SetToolTip(licenseButton, "Show the Shadowsocks Reborn license bundled with this application."); + licenseButton.Click += async (_, _) => await ShowLicenseAsync(); + links.Children.Add(licenseButton); + + var noticesButton = new Button { Content = "Third-party notices" }; + _context.SetToolTip(noticesButton, "Show third-party license and source notices bundled with this application."); + noticesButton.Click += async (_, _) => await ShowThirdPartyNoticesAsync(); + links.Children.Add(noticesButton); + identity.Children.Add(links); + panel.Children.Add(WinUIStyles.CreateCard(identity)); + + var updates = new StackPanel { Spacing = 12 }; + updates.Children.Add(WinUIStyles.CreateSectionTitle("Updates")); + _updateStatus = WinUIStyles.CreateText("Not checked yet."); + updates.Children.Add(_updateStatus); + _updateAtStartupToggle = new ToggleSwitch + { + Header = "Automatically install updates", + OnContent = "On", + OffContent = "Off", + }; + _context.SetToolTip(_updateAtStartupToggle, "Automatically download, verify and install a newer release after the application starts."); + _updateAtStartupToggle.Toggled += OnUpdateAtStartupToggled; + updates.Children.Add(_updateAtStartupToggle); + + _preReleaseToggle = new ToggleSwitch + { + Header = "Include prerelease versions", + OnContent = "On", + OffContent = "Off", + }; + _context.SetToolTip(_preReleaseToggle, "Include prerelease builds when checking GitHub for updates."); + _preReleaseToggle.Toggled += OnPreReleaseToggled; + updates.Children.Add(_preReleaseToggle); + + var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _checkButton = new Button { Content = "Check for Updates" }; + _context.SetToolTip(_checkButton, "Check GitHub for available updates now."); + _checkButton.Click += OnCheckNowClicked; + actions.Children.Add(_checkButton); + _downloadButton = new Button { Content = "Install update", Visibility = Visibility.Collapsed }; + _context.SetToolTip(_downloadButton, "Download, verify and install the selected update automatically."); + _downloadButton.Click += OnDownloadClicked; + actions.Children.Add(_downloadButton); + _skipButton = new Button { Content = "Skip this version", Visibility = Visibility.Collapsed }; + _context.SetToolTip(_skipButton, "Ignore this release in future automatic update checks."); + _skipButton.Click += OnSkipClicked; + actions.Children.Add(_skipButton); + _notNowButton = new Button { Content = "Not now", Visibility = Visibility.Collapsed }; + _context.SetToolTip(_notNowButton, "Hide the current update notification without skipping the release."); + _notNowButton.Click += OnNotNowClicked; + actions.Children.Add(_notNowButton); + _updateProgress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + VerticalAlignment = VerticalAlignment.Center, + }; + _context.SetToolTip(_updateProgress, "Waiting for the update operation to finish…"); + actions.Children.Add(_updateProgress); + updates.Children.Add(actions); + + _releaseHeading = WinUIStyles.CreateSectionTitle("Release notes"); + _releaseHeading.Visibility = Visibility.Collapsed; + updates.Children.Add(_releaseHeading); + _releaseNotes = new TextBox + { + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + MinHeight = 140, + MaxHeight = 300, + Visibility = Visibility.Collapsed, + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe UI"), + }; + ScrollViewer.SetVerticalScrollBarVisibility(_releaseNotes, ScrollBarVisibility.Auto); + _context.SetToolTip(_releaseNotes, "Release notes for the selected available update."); + updates.Children.Add(_releaseNotes); + panel.Children.Add(WinUIStyles.CreateCard(updates)); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + _refreshing = true; + try + { + _updateAtStartupToggle.IsOn = configuration?.autoCheckUpdate == true; + _preReleaseToggle.IsOn = configuration?.checkPreRelease == true; + } + finally + { + _refreshing = false; + } + } + + public async Task CheckForUpdatesAsync() + { + if (_updateChecker is null) + { + _context.ShowInfo("Updates", "Controller is unavailable.", InfoBarSeverity.Error); + return; + } + + _updateFound = false; + SetUpdateBusy(true); + HideUpdateDetails(); + _updateStatus.Text = _context.L("Checking GitHub releases…"); + try + { + await _updateChecker.CheckForVersionUpdate(); + if (!_updateFound) + { + _updateStatus.Text = string.IsNullOrWhiteSpace(_updateChecker.LastCheckError) + ? _context.LF("You are up to date · {0}", ApplicationInfo.Version) + : _context.LF("Update check failed: {0}", _updateChecker.LastCheckError); + } + } + catch (Exception exception) + { + _updateStatus.Text = _context.LF("Update check failed: {0}", exception.Message); + } + finally + { + SetUpdateBusy(false); + } + } + + private async void OnCheckNowClicked(object _, RoutedEventArgs _1) + => await CheckForUpdatesAsync(); + + private void OnUpdateAvailable(object? _, UpdateAvailableEventArgs e) + { + string releaseNotes = e.Release["body"]?.ToString() ?? _context.L("Release notes are unavailable."); + bool isPrerelease = (bool?)e.Release["prerelease"] == true; + _ = DispatcherQueue.TryEnqueue(() => + { + if (_updateChecker is null) + { + return; + } + + _updateFound = true; + _updateStatus.Text = _context.LF("{0} available: {1}", isPrerelease ? _context.L("Pre-release") : _context.L("Release"), _updateChecker.NewReleaseVersion); + _releaseNotes.Text = releaseNotes; + _downloadButton.Visibility = Visibility.Visible; + _skipButton.Visibility = Visibility.Visible; + _notNowButton.Visibility = Visibility.Visible; + _releaseHeading.Visibility = Visibility.Visible; + _releaseNotes.Visibility = Visibility.Visible; + }); + } + + private void OnUpdateCheckCompleted(object? _, EventArgs _1) + { + _ = DispatcherQueue.TryEnqueue(() => + { + if (!_updateFound && _updateChecker is not null) + { + _updateStatus.Text = string.IsNullOrWhiteSpace(_updateChecker.LastCheckError) + ? _context.LF("You are up to date · {0}", ApplicationInfo.Version) + : _context.LF("Update check failed: {0}", _updateChecker.LastCheckError); + } + }); + } + + private async void OnDownloadClicked(object _, RoutedEventArgs _1) + { + if (_updateChecker is null) + { + return; + } + + SetUpdateBusy(true); + try + { + _updateStatus.Text = _context.LF("Installing update {0}…", _updateChecker.NewReleaseVersion); + if (await _updateChecker.DoUpdate()) + { + _context.RequestApplicationExit(); + } + } + catch (Exception exception) + { + _context.ShowInfo("Updates", exception.Message, InfoBarSeverity.Error); + } + finally + { + SetUpdateBusy(false); + } + } + + private void OnSkipClicked(object _, RoutedEventArgs _1) + { + if (_updateChecker is null) + { + return; + } + + _updateChecker.SkipUpdate(); + HideUpdateDetails(); + _updateStatus.Text = _context.L("This version will be skipped."); + } + + private void OnNotNowClicked(object _, RoutedEventArgs _1) + { + HideUpdateDetails(); + _updateStatus.Text = _context.L("Update postponed for this session."); + } + + private void SetUpdateBusy(bool busy) + { + _updateProgress.IsActive = busy; + _updateProgress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + SetUpdateActionsEnabled(!busy); + } + + private void SetUpdateActionsEnabled(bool enabled) + { + _checkButton.IsEnabled = enabled; + _downloadButton.IsEnabled = enabled; + _skipButton.IsEnabled = enabled; + _notNowButton.IsEnabled = enabled; + _preReleaseToggle.IsEnabled = enabled; + _updateAtStartupToggle.IsEnabled = enabled; + } + + private void HideUpdateDetails() + { + _downloadButton.Visibility = Visibility.Collapsed; + _skipButton.Visibility = Visibility.Collapsed; + _notNowButton.Visibility = Visibility.Collapsed; + _releaseHeading.Visibility = Visibility.Collapsed; + _releaseNotes.Visibility = Visibility.Collapsed; + } + + private void OnUpdateAtStartupToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + _context.Controller.ToggleCheckingUpdate(_updateAtStartupToggle.IsOn); + } + + private void OnPreReleaseToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + _context.Controller.ToggleCheckingPreRelease(_preReleaseToggle.IsOn); + } + + private Task ShowLicenseAsync() + => ShowEmbeddedTextAsync( + () => EmbeddedResources.LocalizedProductLicense(_context.Localization.Culture), + "License", + "License resource is unavailable."); + + private Task ShowThirdPartyNoticesAsync() + => ShowEmbeddedTextAsync( + () => EmbeddedResources.LocalizedThirdPartyNotices(_context.Localization.Culture), + "Third-party notices", + "Third-party notices resource is unavailable."); + + private async Task ShowEmbeddedTextAsync(Func readText, string titleKey, string unavailableKey) + { + string text; + try + { + text = readText(); + } + catch (InvalidOperationException exception) + { + _context.ShowInfo(_context.L(titleKey), $"{_context.L(unavailableKey)} {exception.Message}", InfoBarSeverity.Error); + return; + } + + if (string.IsNullOrWhiteSpace(text)) + { + _context.ShowInfo(_context.L(titleKey), _context.L(unavailableKey), InfoBarSeverity.Error); + return; + } + var content = new TextBox + { + Text = text, + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Consolas"), + IsSpellCheckEnabled = false, + MinWidth = 620, + MinHeight = 320, + MaxHeight = 520, + }; + ScrollViewer.SetVerticalScrollBarVisibility(content, ScrollBarVisibility.Auto); + var dialog = new ContentDialog + { + XamlRoot = XamlRoot, + Title = _context.L(titleKey), + Content = content, + CloseButtonText = _context.L("Close"), + DefaultButton = ContentDialogButton.Close, + }; + await dialog.ShowAsync(); + } + + private void OpenUrl(string url) + { + try + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + catch (Exception exception) + { + _context.ShowInfo("About", exception.Message, InfoBarSeverity.Error); + } + } +} diff --git a/Shadowsocks.WinUI/Pages/DnsPage.cs b/Shadowsocks.WinUI/Pages/DnsPage.cs new file mode 100644 index 000000000..582c34a08 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/DnsPage.cs @@ -0,0 +1,1934 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class DnsPage : Page, IRefreshablePage, IDisposable +{ + private sealed record ResolverChoice(DnsCryptResolverInfo Resolver) + { + public string Name => Resolver.Name; + + public override string ToString() + { + string flag = string.IsNullOrWhiteSpace(Resolver.FlagEmoji) ? "•" : Resolver.FlagEmoji; + string protocol = string.IsNullOrWhiteSpace(Resolver.Protocol) ? "DNS" : Resolver.Protocol; + string latency = Resolver.LatencyMs is int latencyMs ? $"{latencyMs} ms" : "—"; + return $"{flag} {Resolver.Name} · {protocol} · {latency}"; + } + } + + private sealed record SystemDnsSnapshot(IReadOnlyList Adapters, IReadOnlyList Servers); + + private sealed record DohPreset(string Name, string Url) + { + public bool IsCustom => string.IsNullOrWhiteSpace(Url); + public override string ToString() => Name; + } + + private sealed record DnsServerPreset(string Name, string Primary, string Fallback, bool IsCustom = false) + { + public bool IsOriginal => !IsCustom && string.IsNullOrWhiteSpace(Primary); + public override string ToString() => Name; + } + + private sealed record ResolverCountryFilter(string CountryCode, string Label) + { + public override string ToString() => Label; + } + + private sealed record ResolverProtocolFilter(string Protocol, string Label) + { + public override string ToString() => Label; + } + + private readonly WinUIPageContext _context; + private readonly TextBlock _systemDnsAdapters; + private readonly TextBlock _systemDnsServers; + private readonly TextBlock _effectiveDnsMode; + private readonly TextBlock _dnsInterceptionStatus; + private readonly RadioButton _systemMode; + private readonly RadioButton _directMode; + private readonly RadioButton _proxyMode; + private readonly RadioButton _customDohMode; + private readonly RadioButton _dnsCryptMode; + private readonly IReadOnlyList _dohPresets; + private readonly IReadOnlyList _dnsServerPresets; + private readonly ComboBox _customDohProvider; + private readonly ComboBox _directDnsProvider; + private readonly TextBox _directDnsPrimaryServer; + private readonly TextBox _directDnsFallbackServer; + private readonly ToggleSwitch _directDnsRouteThroughShadowsocks; + private readonly TextBox _customDohUrl; + private readonly ToggleSwitch _customDohRouteThroughShadowsocks; + private readonly Border _systemSettingsCard; + private readonly Border _directSettingsCard; + private readonly Border _proxySettingsCard; + private readonly Border _customDohSettingsCard; + private readonly Border _dnsCryptOperationCard; + private readonly Border _dnsCryptStatusCard; + private readonly Border _dnsCryptSettingsCard; + private readonly TextBlock _runtimeStatus; + private readonly TextBlock _installedVersion; + private readonly TextBlock _latestVersion; + private readonly TextBlock _localPort; + private readonly TextBlock _coverage; + private readonly TextBlock _privacyDnsInterception; + private readonly TextBlock _privacyUpstream; + private readonly TextBlock _privacyTransport; + private readonly TextBlock _privacyFallback; + private readonly TextBlock _privacyBootstrap; + private readonly TextBlock _privacyRoute; + private readonly TextBlock _privacySelfTest; + private readonly Button _privacyTestButton; + private readonly Button _installButton; + private readonly Button _checkButton; + private readonly Button _updateButton; + private readonly Button _restartButton; + private readonly Button _testButton; + private readonly Button _reinstallButton; + private readonly Button _removeButton; + private readonly ToggleSwitch _autoUpdate; + private readonly ToggleSwitch _requireDnssec; + private readonly ToggleSwitch _requireNoLog; + private readonly ToggleSwitch _requireNoFilter; + private readonly ToggleSwitch _ipv6Servers; + private readonly ToggleSwitch _routeThroughShadowsocks; + private readonly RadioButton _resolverAutomatic; + private readonly RadioButton _resolverSelected; + private readonly StackPanel _automaticResolverPanel; + private readonly ListView _automaticResolverList; + private readonly TextBlock _automaticResolverStatus; + private readonly StackPanel _manualResolverPanel; + private readonly TextBox _resolverSearchBox; + private readonly ComboBox _resolverProtocolFilter; + private readonly ComboBox _resolverCountryFilter; + private readonly ComboBox _resolverAddressFamilyFilter; + private readonly ToggleSwitch _resolverDnssecFilter; + private readonly ToggleSwitch _resolverNoLogFilter; + private readonly ToggleSwitch _resolverNoFilterFilter; + private readonly ListView _resolverList; + private readonly Button _loadResolversButton; + private readonly TextBlock _resolverListStatus; + private readonly ProgressRing _progressRing; + private readonly ProgressBar _progressBar; + private readonly TextBlock _progressText; + private readonly Button _cancelButton; + + private Action? _cancelOperation; + private Action? _cancelSettingsApply; + private Action? _cancelResolverLatency; + private bool _refreshing; + private bool _busy; + private bool _dnsCryptOperationActive; + private bool _resolverCatalogLoaded; + private bool _resolverCatalogLoading; + private bool _updatingResolverList; + private bool _settingsDirty; + private bool _customDohDirty; + private bool _directDnsDirty; + private bool _updatingDohPreset; + private bool _updatingDirectDnsPreset; + private DnsCryptResolverInfo[] _resolverCatalog = Array.Empty(); + private readonly HashSet _manualResolverNames = new(StringComparer.OrdinalIgnoreCase); + private long _settingsApplyGeneration; + private string? _privacySelfTestMessage; + private bool _disposed; + + internal DnsPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "DNS", + "Manage DNS policy and the optional DNSCrypt Proxy component downloaded from the official upstream release.", + out StackPanel panel); + + var dnsStateStack = new StackPanel { Spacing = 10 }; + dnsStateStack.Children.Add(WinUIStyles.CreateSectionTitle("DNS status")); + dnsStateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Active adapters", out _systemDnsAdapters)); + dnsStateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Windows DNS servers", out _systemDnsServers)); + dnsStateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Shadowsocks DNS policy", out _effectiveDnsMode)); + dnsStateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Transparent interception", out _dnsInterceptionStatus)); + TextBlock dnsStateNote = WinUIStyles.CreateText( + "System DNS information is read from active Windows network adapters; Shadowsocks does not change adapter DNS addresses."); + dnsStateNote.Opacity = 0.72; + dnsStateStack.Children.Add(dnsStateNote); + panel.Children.Add(WinUIStyles.CreateCard(dnsStateStack)); + + var modeStack = new StackPanel { Spacing = 10 }; + modeStack.Children.Add(WinUIStyles.CreateSectionTitle("DNS mode")); + _systemMode = new RadioButton { Content = "System DNS", GroupName = "DnsMode" }; + _directMode = CreateMode("Direct DNS"); + _proxyMode = CreateMode("DNS through Shadowsocks"); + _customDohMode = CreateMode("Custom DoH"); + _dnsCryptMode = new RadioButton { Content = "DNSCrypt", GroupName = "DnsMode" }; + _systemMode.Checked += OnSystemModeChecked; + _directMode.Checked += OnDirectModeChecked; + _proxyMode.Checked += OnProxyModeChecked; + _customDohMode.Checked += OnCustomDohModeChecked; + _dnsCryptMode.Checked += OnDnsCryptModeChecked; + modeStack.Children.Add(_systemMode); + modeStack.Children.Add(_directMode); + modeStack.Children.Add(_proxyMode); + modeStack.Children.Add(_customDohMode); + modeStack.Children.Add(_dnsCryptMode); + + _context.SetToolTip(_systemMode, "Use DNS servers configured by Windows without Shadowsocks DNS redirection."); + _context.SetToolTip(_directMode, "Send captured DNS traffic directly. You can keep the original destination or specify an IPv4/IPv6 DNS server; transparent enforcement requires Administrator Mode."); + _context.SetToolTip(_proxyMode, "Route captured DNS traffic through the active Shadowsocks server. Transparent enforcement requires Administrator Mode."); + _context.SetToolTip(_customDohMode, "Convert captured DNS queries to DNS-over-HTTPS using the configured HTTPS endpoint."); + _context.SetToolTip(_dnsCryptMode, "Use DNSCrypt Proxy for Shadowsocks-managed DNS and transparent DNS capture when available."); + + TextBlock stageNotice = WinUIStyles.CreateText( + "Administrator Mode transparently applies DNS through Shadowsocks, Custom DoH and DNSCrypt to captured UDP/TCP DNS traffic. " + + "In User Mode DNSCrypt still runs for Shadowsocks-managed DNS, but system-wide interception is unavailable."); + stageNotice.Opacity = 0.72; + modeStack.Children.Add(stageNotice); + panel.Children.Add(WinUIStyles.CreateCard(modeStack)); + + var systemSettingsStack = new StackPanel { Spacing = 8 }; + systemSettingsStack.Children.Add(WinUIStyles.CreateSectionTitle("System DNS settings")); + systemSettingsStack.Children.Add(WinUIStyles.CreateText( + "Windows DNS servers from active network adapters are used without modification.")); + _systemSettingsCard = WinUIStyles.CreateCard(systemSettingsStack); + panel.Children.Add(_systemSettingsCard); + + var directSettingsStack = new StackPanel { Spacing = 8 }; + directSettingsStack.Children.Add(WinUIStyles.CreateSectionTitle("Direct DNS settings")); + _dnsServerPresets = + [ + new DnsServerPreset(_context.L("Original DNS destination"), string.Empty, string.Empty), + new DnsServerPreset("Cloudflare", "1.1.1.1", "1.0.0.1"), + new DnsServerPreset("Google Public DNS", "8.8.8.8", "8.8.4.4"), + new DnsServerPreset("Quad9", "9.9.9.9", "149.112.112.112"), + new DnsServerPreset("AdGuard DNS", "94.140.14.14", "94.140.15.15"), + new DnsServerPreset("OpenDNS", "208.67.222.222", "208.67.220.220"), + new DnsServerPreset(_context.L("Custom"), string.Empty, string.Empty, IsCustom: true), + ]; + _directDnsProvider = new ComboBox + { + Header = "DNS provider", + ItemsSource = _dnsServerPresets, + SelectedIndex = 0, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _directDnsProvider.SelectionChanged += OnDirectDnsProviderChanged; + _context.SetToolTip(_directDnsProvider, "Choose a known DNS provider, keep the original DNS destination, or select Custom to enter primary and fallback addresses."); + directSettingsStack.Children.Add(_directDnsProvider); + + _directDnsPrimaryServer = new TextBox + { + Header = "Primary DNS server", + PlaceholderText = "1.1.1.1", + }; + _directDnsFallbackServer = new TextBox + { + Header = "Fallback DNS server", + PlaceholderText = "1.0.0.1", + }; + _directDnsPrimaryServer.TextChanged += OnDirectDnsTextChanged; + _directDnsFallbackServer.TextChanged += OnDirectDnsTextChanged; + _directDnsPrimaryServer.LostFocus += OnDirectDnsServerLostFocus; + _directDnsFallbackServer.LostFocus += OnDirectDnsServerLostFocus; + _context.SetToolTip(_directDnsPrimaryServer, "Primary IPv4 or IPv6 DNS server. In Custom mode this address is used first."); + _context.SetToolTip(_directDnsFallbackServer, "Fallback IPv4 or IPv6 DNS server. UDP queries are retried here when the primary resolver does not answer; TCP falls back when the primary connection fails."); + directSettingsStack.Children.Add(_directDnsPrimaryServer); + directSettingsStack.Children.Add(_directDnsFallbackServer); + _directDnsRouteThroughShadowsocks = CreateToggle("Route Direct DNS through Shadowsocks"); + _directDnsRouteThroughShadowsocks.Toggled += OnDirectDnsRouteChanged; + _context.SetToolTip( + _directDnsRouteThroughShadowsocks, + "Send queries to the selected Direct DNS primary/fallback servers through the active local Shadowsocks SOCKS5 proxy instead of connecting to them directly."); + directSettingsStack.Children.Add(_directDnsRouteThroughShadowsocks); + directSettingsStack.Children.Add(WinUIStyles.CreateText( + "Administrator Mode redirects UDP/TCP DNS queries to the selected resolver. The fallback server is used only after the primary path fails or does not answer in time.")); + _directSettingsCard = WinUIStyles.CreateCard(directSettingsStack); + _directSettingsCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_directSettingsCard); + + var proxySettingsStack = new StackPanel { Spacing = 8 }; + proxySettingsStack.Children.Add(WinUIStyles.CreateSectionTitle("DNS through Shadowsocks settings")); + proxySettingsStack.Children.Add(WinUIStyles.CreateText( + "Captured DNS queries use the active Shadowsocks route; no separate resolver endpoint is required.")); + _proxySettingsCard = WinUIStyles.CreateCard(proxySettingsStack); + _proxySettingsCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_proxySettingsCard); + + var customDohSettingsStack = new StackPanel { Spacing = 10 }; + customDohSettingsStack.Children.Add(WinUIStyles.CreateSectionTitle("DNS-over-HTTPS settings")); + _dohPresets = + [ + new DohPreset(_context.L("Custom"), string.Empty), + new DohPreset("Cloudflare", "https://cloudflare-dns.com/dns-query"), + new DohPreset("Google Public DNS", "https://dns.google/dns-query"), + new DohPreset(_context.L("Quad9 Secure"), "https://dns.quad9.net/dns-query"), + new DohPreset(_context.L("Quad9 Unfiltered"), "https://dns10.quad9.net/dns-query"), + new DohPreset("AdGuard DNS", "https://dns.adguard-dns.com/dns-query"), + new DohPreset(_context.L("AdGuard DNS Unfiltered"), "https://unfiltered.adguard-dns.com/dns-query"), + new DohPreset("Mullvad DNS", "https://dns.mullvad.net/dns-query"), + ]; + _customDohProvider = new ComboBox + { + Header = "DoH provider", + ItemsSource = _dohPresets, + SelectedIndex = 0, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _customDohProvider.SelectionChanged += OnCustomDohProviderChanged; + _context.SetToolTip(_customDohProvider, "Choose a well-known RFC 8484 DNS-over-HTTPS provider or select Custom to enter your own HTTPS endpoint."); + customDohSettingsStack.Children.Add(_customDohProvider); + + _customDohUrl = new TextBox + { + Header = "DoH URL", + PlaceholderText = "https://dns.example/dns-query", + }; + _customDohUrl.TextChanged += (_, _) => + { + if (!_refreshing && !_updatingDohPreset) + { + _customDohDirty = true; + SyncDohPresetFromUrl(_customDohUrl.Text); + } + }; + _customDohUrl.LostFocus += OnCustomDohUrlLostFocus; + _context.SetToolTip(_customDohUrl, "Enter an absolute HTTPS DNS-over-HTTPS endpoint. Changes are applied automatically when you leave the field."); + customDohSettingsStack.Children.Add(_customDohUrl); + _customDohRouteThroughShadowsocks = CreateToggle("Route Custom DoH through Shadowsocks"); + _customDohRouteThroughShadowsocks.Toggled += OnCustomDohRouteChanged; + _context.SetToolTip(_customDohRouteThroughShadowsocks, "Send the HTTPS connection to the Custom DoH provider through the local Shadowsocks SOCKS5 proxy instead of connecting directly."); + customDohSettingsStack.Children.Add(_customDohRouteThroughShadowsocks); + customDohSettingsStack.Children.Add(WinUIStyles.CreateText( + "DNS queries are sent as DNS-over-HTTPS to the configured endpoint.")); + _customDohSettingsCard = WinUIStyles.CreateCard(customDohSettingsStack); + _customDohSettingsCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_customDohSettingsCard); + + var progressStack = new StackPanel { Spacing = 8 }; + progressStack.Children.Add(WinUIStyles.CreateSectionTitle("DNSCrypt operation")); + var progressHeader = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 }; + _progressRing = new ProgressRing + { + Width = 24, + Height = 24, + IsActive = false, + }; + _progressText = WinUIStyles.CreateText("Ready"); + progressHeader.Children.Add(_progressRing); + progressHeader.Children.Add(_progressText); + _progressBar = new ProgressBar + { + Minimum = 0, + Maximum = 100, + IsIndeterminate = false, + Value = 0, + }; + _cancelButton = new Button { Content = "Cancel", IsEnabled = false }; + _cancelButton.Click += (_, _) => _cancelOperation?.Invoke(); + progressStack.Children.Add(progressHeader); + progressStack.Children.Add(_progressBar); + progressStack.Children.Add(_cancelButton); + _context.SetToolTip(_cancelButton, "Cancel the current DNSCrypt install, update, validation, or runtime operation."); + _dnsCryptOperationCard = WinUIStyles.CreateCard(progressStack); + _dnsCryptOperationCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_dnsCryptOperationCard); + + var statusStack = new StackPanel { Spacing = 10 }; + statusStack.Children.Add(WinUIStyles.CreateSectionTitle("DNSCrypt Proxy")); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Status", out _runtimeStatus)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Installed version", out _installedVersion)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Latest version", out _latestVersion)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Local port", out _localPort)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Coverage", out _coverage)); + statusStack.Children.Add(WinUIStyles.CreateSectionTitle("DNS privacy")); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("DNS interception", out _privacyDnsInterception)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Upstream", out _privacyUpstream)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Secure transport", out _privacyTransport)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Plaintext DNS fallback", out _privacyFallback)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Plaintext bootstrap", out _privacyBootstrap)); + _context.SetToolTip(_privacyBootstrap, "The active DNSCrypt runtime never uses plaintext or system DNS. Resolver-catalog refresh uses Cloudflare DoH through Shadowsocks, with Google DoH as fallback, and publishes the catalog only after Minisign verification."); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Shadowsocks route", out _privacyRoute)); + statusStack.Children.Add(WinUIStyles.CreateKeyValueRow("Privacy self-test", out _privacySelfTest)); + _privacyTestButton = CreateButton("Run privacy self-test", TestDnsPrivacyAsync); + _context.SetToolTip(_privacyTestButton, "Verify the local DNSCrypt health check, Administrator DNS interception, fail-closed policy, system-DNS isolation and active runtime bootstrap configuration."); + statusStack.Children.Add(_privacyTestButton); + + var buttonRows = new StackPanel { Spacing = 8 }; + var primaryButtonRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + var maintenanceButtonRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _installButton = CreateButton("Install DNSCrypt Proxy", InstallAsync); + _checkButton = CreateButton("Check for DNSCrypt updates", CheckUpdateAsync); + _updateButton = CreateButton("Update", UpdateAsync); + _restartButton = CreateButton("Restart", RestartAsync); + _testButton = CreateButton("Test DNSCrypt", TestDnsCryptAsync); + _reinstallButton = CreateButton("Reinstall", ReinstallAsync); + _removeButton = CreateButton("Remove", RemoveAsync); + _context.SetToolTip(_installButton, "Download, verify, and install the official Windows x64 DNSCrypt Proxy release."); + _context.SetToolTip(_checkButton, "Check the official DNSCrypt Proxy release channel for a newer version."); + _context.SetToolTip(_updateButton, "Install the newest verified DNSCrypt Proxy version and keep the previous version for rollback."); + _context.SetToolTip(_restartButton, "Restart DNSCrypt Proxy with the currently saved settings."); + _context.SetToolTip(_testButton, "Send a real DNS query through the local DNSCrypt listener and validate the response."); + _context.SetToolTip(_reinstallButton, "Download and verify the current DNSCrypt Proxy version again, then replace the installed copy."); + _context.SetToolTip(_removeButton, "Stop DNSCrypt Proxy, switch away from DNSCrypt mode, and remove the downloaded component."); + primaryButtonRow.Children.Add(_installButton); + primaryButtonRow.Children.Add(_checkButton); + primaryButtonRow.Children.Add(_updateButton); + maintenanceButtonRow.Children.Add(_restartButton); + maintenanceButtonRow.Children.Add(_testButton); + maintenanceButtonRow.Children.Add(_reinstallButton); + maintenanceButtonRow.Children.Add(_removeButton); + buttonRows.Children.Add(primaryButtonRow); + buttonRows.Children.Add(maintenanceButtonRow); + statusStack.Children.Add(buttonRows); + _dnsCryptStatusCard = WinUIStyles.CreateCard(statusStack); + _dnsCryptStatusCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_dnsCryptStatusCard); + + var settingsStack = new StackPanel { Spacing = 10 }; + settingsStack.Children.Add(WinUIStyles.CreateSectionTitle("DNSCrypt settings")); + _autoUpdate = CreateToggle("Automatically update DNSCrypt Proxy"); + _requireDnssec = CreateToggle("Require DNSSEC"); + _requireNoLog = CreateToggle("Require no-log resolvers"); + _requireNoFilter = CreateToggle("Require unfiltered resolvers"); + _ipv6Servers = CreateToggle("Use IPv6 resolvers"); + _routeThroughShadowsocks = CreateToggle("Route DNSCrypt through Shadowsocks"); + _context.SetToolTip(_autoUpdate, "Automatically check for and install verified DNSCrypt Proxy updates in the background."); + _context.SetToolTip(_requireDnssec, "Only use resolvers that advertise DNSSEC validation support."); + _context.SetToolTip(_requireNoLog, "Only use resolvers whose catalog metadata states that they do not keep logs."); + _context.SetToolTip(_requireNoFilter, "Only use resolvers whose catalog metadata states that they do not filter DNS answers."); + _context.SetToolTip(_ipv6Servers, "Allow DNSCrypt Proxy to use resolver endpoints reachable over IPv6."); + _context.SetToolTip(_routeThroughShadowsocks, "Send DNSCrypt Proxy upstream connections through the local Shadowsocks proxy. Server and forward-proxy endpoints must use IP addresses to avoid bootstrap recursion."); + _autoUpdate.Toggled += OnDnsCryptSettingChanged; + _requireDnssec.Toggled += OnDnsCryptSettingChanged; + _requireNoLog.Toggled += OnDnsCryptSettingChanged; + _requireNoFilter.Toggled += OnDnsCryptSettingChanged; + _ipv6Servers.Toggled += OnDnsCryptSettingChanged; + _routeThroughShadowsocks.Toggled += OnDnsCryptSettingChanged; + settingsStack.Children.Add(_autoUpdate); + settingsStack.Children.Add(_requireDnssec); + settingsStack.Children.Add(_requireNoLog); + settingsStack.Children.Add(_requireNoFilter); + settingsStack.Children.Add(_ipv6Servers); + settingsStack.Children.Add(_routeThroughShadowsocks); + TextBlock bootstrapNote = WinUIStyles.CreateText( + "When DNSCrypt is routed through Shadowsocks the Shadowsocks server and forward-proxy endpoints " + + "must use IP addresses to prevent DNS bootstrap recursion."); + bootstrapNote.Opacity = 0.72; + settingsStack.Children.Add(bootstrapNote); + TextBlock leakGuardNote = WinUIStyles.CreateText( + "In Administrator Mode, DNSCrypt always blocks intercepted plaintext DNS while starting, restarting, or unavailable."); + leakGuardNote.Opacity = 0.72; + settingsStack.Children.Add(leakGuardNote); + + settingsStack.Children.Add(WinUIStyles.CreateSectionTitle("Resolver selection")); + _resolverAutomatic = new RadioButton + { + Content = "Automatic", + GroupName = "DnsCryptResolverSelection", + }; + _resolverSelected = new RadioButton + { + Content = "Manual selection", + GroupName = "DnsCryptResolverSelection", + }; + _resolverAutomatic.Checked += OnResolverSelectionChanged; + _resolverSelected.Checked += OnResolverSelectionChanged; + _context.SetToolTip(_resolverAutomatic, "Automatically select a resolver from DNSCrypt Proxy's signed catalog using the DNSSEC, no-log, unfiltered and address-family settings."); + _context.SetToolTip(_resolverSelected, "Choose one or more DNSCrypt or DoH resolvers manually from the signed upstream catalog."); + settingsStack.Children.Add(_resolverAutomatic); + settingsStack.Children.Add(_resolverSelected); + + _automaticResolverPanel = new StackPanel { Spacing = 8 }; + _automaticResolverPanel.Children.Add(WinUIStyles.CreateSectionTitle("Automatically selected resolvers")); + _automaticResolverStatus = WinUIStyles.CreateText("Automatic mode selects a filtered resolver from DNSCrypt Proxy's signed catalog."); + _automaticResolverStatus.Opacity = 0.72; + _automaticResolverPanel.Children.Add(_automaticResolverStatus); + _automaticResolverList = new ListView + { + SelectionMode = ListViewSelectionMode.None, + MinHeight = 44, + MaxHeight = 150, + IsItemClickEnabled = false, + }; + _context.SetToolTip(_automaticResolverList, "Shows the resolvers currently chosen by automatic DNSCrypt selection."); + _automaticResolverPanel.Children.Add(_automaticResolverList); + settingsStack.Children.Add(_automaticResolverPanel); + + _manualResolverPanel = new StackPanel { Spacing = 8 }; + var resolverCatalogHeader = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + resolverCatalogHeader.Children.Add(WinUIStyles.CreateSectionTitle("Available resolvers")); + _loadResolversButton = CreateButton("Load resolver list", LoadResolversAsync); + _context.SetToolTip(_loadResolversButton, "Reload resolver metadata from DNSCrypt Proxy's signed upstream catalog. Latency is measured separately in the background."); + resolverCatalogHeader.Children.Add(_loadResolversButton); + _manualResolverPanel.Children.Add(resolverCatalogHeader); + _resolverSearchBox = new TextBox + { + PlaceholderText = "Search resolvers", + }; + _resolverSearchBox.TextChanged += (_, _) => RebuildResolverList(); + _context.SetToolTip(_resolverSearchBox, "Filter resolvers by name, country code, country name, or description."); + _manualResolverPanel.Children.Add(_resolverSearchBox); + + var resolverFilters = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _resolverProtocolFilter = new ComboBox + { + Header = "Protocol", + ItemsSource = new[] + { + new ResolverProtocolFilter(string.Empty, _context.L("All protocols")), + new ResolverProtocolFilter("DNSCrypt", "DNSCrypt"), + new ResolverProtocolFilter("DoH", "DoH"), + }, + SelectedIndex = 0, + MinWidth = 150, + }; + _resolverCountryFilter = new ComboBox + { + Header = "Country", + MinWidth = 180, + }; + _resolverAddressFamilyFilter = new ComboBox + { + Header = "Address family", + ItemsSource = new[] { _context.L("Any address family"), "IPv4", "IPv6" }, + SelectedIndex = 0, + MinWidth = 150, + }; + _resolverProtocolFilter.SelectionChanged += (_, _) => + { + RefreshResolverCountryFilter(); + RebuildResolverList(); + StartResolverLatencyRefresh(); + }; + _resolverCountryFilter.SelectionChanged += (_, _) => RebuildResolverList(); + _resolverAddressFamilyFilter.SelectionChanged += (_, _) => RebuildResolverList(); + _context.SetToolTip(_resolverProtocolFilter, "Filter the signed resolver catalog by transport protocol: DNSCrypt or DNS-over-HTTPS."); + _context.SetToolTip(_resolverCountryFilter, "Show resolvers from a selected country. Country is resolved from the resolver endpoint IP."); + _context.SetToolTip(_resolverAddressFamilyFilter, "Filter resolver endpoints by IPv4 or IPv6."); + resolverFilters.Children.Add(_resolverProtocolFilter); + resolverFilters.Children.Add(_resolverCountryFilter); + resolverFilters.Children.Add(_resolverAddressFamilyFilter); + _manualResolverPanel.Children.Add(resolverFilters); + + var resolverPropertyFilters = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 16 }; + _resolverDnssecFilter = CreateToggle("DNSSEC only"); + _resolverNoLogFilter = CreateToggle("No-log only"); + _resolverNoFilterFilter = CreateToggle("Unfiltered only"); + _resolverDnssecFilter.Toggled += (_, _) => RebuildResolverList(); + _resolverNoLogFilter.Toggled += (_, _) => RebuildResolverList(); + _resolverNoFilterFilter.Toggled += (_, _) => RebuildResolverList(); + _context.SetToolTip(_resolverDnssecFilter, "Only show resolvers that advertise DNSSEC validation."); + _context.SetToolTip(_resolverNoLogFilter, "Only show resolvers whose catalog metadata declares no logging."); + _context.SetToolTip(_resolverNoFilterFilter, "Only show resolvers that do not filter DNS answers."); + resolverPropertyFilters.Children.Add(_resolverDnssecFilter); + resolverPropertyFilters.Children.Add(_resolverNoLogFilter); + resolverPropertyFilters.Children.Add(_resolverNoFilterFilter); + _manualResolverPanel.Children.Add(resolverPropertyFilters); + + RefreshResolverCountryFilter(); + _resolverListStatus = WinUIStyles.CreateText("Resolver list is loaded from DNSCrypt Proxy's signed upstream catalog. Latency is measured in the background; the active resolver uses DNSCrypt Proxy's actual RTT when available."); + _resolverListStatus.Opacity = 0.72; + _manualResolverPanel.Children.Add(_resolverListStatus); + _resolverList = new ListView + { + SelectionMode = ListViewSelectionMode.Multiple, + MinHeight = 140, + MaxHeight = 300, + }; + _resolverList.SelectionChanged += OnResolverListSelectionChanged; + _context.SetToolTip(_resolverList, "Select the DNSCrypt or DoH resolvers DNSCrypt Proxy may use in manual-selection mode. Ping is measured asynchronously; the active resolver is replaced with DNSCrypt Proxy's actual RTT when available."); + _manualResolverPanel.Children.Add(_resolverList); + settingsStack.Children.Add(_manualResolverPanel); + + TextBlock automaticApplyNote = WinUIStyles.CreateText("DNS settings are saved and applied automatically."); + automaticApplyNote.Opacity = 0.72; + settingsStack.Children.Add(automaticApplyNote); + _dnsCryptSettingsCard = WinUIStyles.CreateCard(settingsStack); + _dnsCryptSettingsCard.Visibility = Visibility.Collapsed; + panel.Children.Add(_dnsCryptSettingsCard); + + WinUILocalization.Apply(Content, _context.Localization); + Refresh(); + } + + public void Refresh() + { + if (_context.Controller is null) + return; + + DnsCryptManagementStatus status; + try + { + status = _context.Controller.GetDnsCryptManagementStatus(); + } + catch (Exception exception) + { + _runtimeStatus.Text = _context.LF("Failed: {0}", exception.Message); + return; + } + + _refreshing = true; + try + { + TrafficCaptureStatus traffic = _context.Controller.GetTrafficCaptureStatus(); + DnsPolicyConfig policy = _context.Controller.GetCurrentConfiguration().dnsPolicy ?? new DnsPolicyConfig(); + DnsCryptConfig config = policy.dnsCrypt ?? new DnsCryptConfig(); + if (!_directDnsDirty) + { + _directDnsPrimaryServer.Text = policy.directDnsServer ?? string.Empty; + _directDnsFallbackServer.Text = policy.directDnsFallbackServer ?? string.Empty; + _directDnsRouteThroughShadowsocks.IsOn = policy.directDnsRouteThroughShadowsocks; + SyncDirectDnsPresetFromAddresses(_directDnsPrimaryServer.Text, _directDnsFallbackServer.Text); + } + if (!_customDohDirty) + { + _customDohUrl.Text = policy.customDohUrl ?? string.Empty; + _customDohRouteThroughShadowsocks.IsOn = policy.customDohRouteThroughShadowsocks; + SyncDohPresetFromUrl(_customDohUrl.Text); + } + + ApplyDnsSystemState(status.Mode, traffic); + ApplyModeState(status.Mode); + ApplyModeSpecificVisibility(status.Mode); + ApplyRuntimeState(status, traffic, policy); + ApplyConfigurationState(config); + MergeActiveResolverData(status); + ApplyResolverPresentation(status); + ApplyControlState(status, traffic); + } + finally + { + _refreshing = false; + } + } + + + private void ApplyDnsSystemState(DnsPolicyMode mode, TrafficCaptureStatus traffic) + { + SystemDnsSnapshot snapshot = GetSystemDnsSnapshot(); + _systemDnsAdapters.Text = snapshot.Adapters.Count > 0 + ? string.Join(", ", snapshot.Adapters) + : _context.L("No active network adapters detected."); + _systemDnsServers.Text = snapshot.Servers.Count > 0 + ? string.Join(", ", snapshot.Servers) + : _context.L("No active DNS servers detected."); + _effectiveDnsMode.Text = _context.L(mode switch + { + DnsPolicyMode.Direct => "Direct DNS", + DnsPolicyMode.Proxy => "DNS through Shadowsocks", + DnsPolicyMode.CustomDoh => "Custom DoH", + DnsPolicyMode.DnsCrypt => "DNSCrypt", + _ => "System DNS", + }); + _dnsInterceptionStatus.Text = traffic.DnsInterceptionActive + ? _context.L("Active") + : traffic.RuntimeMode switch + { + TrafficRuntimeMode.User => _context.L("Unavailable in User Mode"), + TrafficRuntimeMode.Game => _context.L("Paused in Game Mode"), + _ => _context.L("Inactive"), + }; + } + + private static SystemDnsSnapshot GetSystemDnsSnapshot() + { + try + { + var adapters = new SortedSet(StringComparer.CurrentCultureIgnoreCase); + var servers = new SortedSet(StringComparer.OrdinalIgnoreCase); + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + if (networkInterface.OperationalStatus != OperationalStatus.Up + || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback) + { + continue; + } + + try + { + var dnsAddresses = networkInterface.GetIPProperties().DnsAddresses; + if (dnsAddresses.Count == 0) + continue; + + adapters.Add(networkInterface.Name); + foreach (var address in dnsAddresses) + servers.Add(address.ToString()); + } + catch (NetworkInformationException) + { + // Ignore an adapter that disappears while Windows network state is being enumerated. + } + } + + return new SystemDnsSnapshot(adapters.ToArray(), servers.ToArray()); + } + catch (NetworkInformationException) + { + return new SystemDnsSnapshot(Array.Empty(), Array.Empty()); + } + } + + private void ApplyModeSpecificVisibility(DnsPolicyMode mode) + { + bool pendingDnsCryptActivation = _dnsCryptOperationActive && mode != DnsPolicyMode.DnsCrypt; + _systemSettingsCard.Visibility = mode == DnsPolicyMode.System && !pendingDnsCryptActivation ? Visibility.Visible : Visibility.Collapsed; + _directSettingsCard.Visibility = mode == DnsPolicyMode.Direct && !pendingDnsCryptActivation ? Visibility.Visible : Visibility.Collapsed; + _proxySettingsCard.Visibility = mode == DnsPolicyMode.Proxy && !pendingDnsCryptActivation ? Visibility.Visible : Visibility.Collapsed; + _customDohSettingsCard.Visibility = mode == DnsPolicyMode.CustomDoh && !pendingDnsCryptActivation ? Visibility.Visible : Visibility.Collapsed; + + bool dnsCryptMode = mode == DnsPolicyMode.DnsCrypt; + _dnsCryptOperationCard.Visibility = dnsCryptMode || _dnsCryptOperationActive ? Visibility.Visible : Visibility.Collapsed; + _dnsCryptStatusCard.Visibility = dnsCryptMode ? Visibility.Visible : Visibility.Collapsed; + _dnsCryptSettingsCard.Visibility = dnsCryptMode ? Visibility.Visible : Visibility.Collapsed; + } + + private void ApplyModeState(DnsPolicyMode mode) + { + bool pendingDnsCryptActivation = _dnsCryptOperationActive && mode != DnsPolicyMode.DnsCrypt; + _systemMode.IsChecked = mode == DnsPolicyMode.System && !pendingDnsCryptActivation; + _directMode.IsChecked = mode == DnsPolicyMode.Direct && !pendingDnsCryptActivation; + _proxyMode.IsChecked = mode == DnsPolicyMode.Proxy && !pendingDnsCryptActivation; + _customDohMode.IsChecked = mode == DnsPolicyMode.CustomDoh && !pendingDnsCryptActivation; + _dnsCryptMode.IsChecked = mode == DnsPolicyMode.DnsCrypt || pendingDnsCryptActivation; + } + + private void ApplyRuntimeState(DnsCryptManagementStatus status, TrafficCaptureStatus traffic, DnsPolicyConfig policy) + { + _runtimeStatus.Text = status.Runtime.State switch + { + DnsCryptRuntimeState.NotInstalled => _context.L("Not installed"), + DnsCryptRuntimeState.Stopped => _context.L("Stopped"), + DnsCryptRuntimeState.Starting => _context.L("Starting"), + DnsCryptRuntimeState.Running => _context.L("Running"), + DnsCryptRuntimeState.Updating => _context.L("Updating"), + _ => _context.L("Failed"), + }; + _installedVersion.Text = status.Component.IsInstalled + ? status.Component.ActiveVersion?.ToString() ?? "—" + : _context.L("Not installed"); + _latestVersion.Text = status.LatestVersion?.ToString() ?? "—"; + _localPort.Text = status.Runtime.IsServing && status.Runtime.Port > 0 + ? $"127.0.0.1:{status.Runtime.Port}" + : "—"; + _coverage.Text = status.Mode switch + { + DnsPolicyMode.System => _context.L("System DNS is unchanged."), + DnsPolicyMode.Direct => traffic.RuntimeMode == TrafficRuntimeMode.Admin + ? policy.directDnsRouteThroughShadowsocks + ? _context.L("Direct DNS queries are routed to the selected resolver through Shadowsocks.") + : _context.L("DNS is forced direct and bypasses Shadowsocks routing.") + : _context.L("Direct DNS is selected; User Mode does not intercept system DNS."), + DnsPolicyMode.Proxy => traffic.RuntimeMode == TrafficRuntimeMode.Admin && traffic.DnsInterceptionActive + ? _context.L("DNS traffic is routed through Shadowsocks for captured applications.") + : _context.L("DNS through Shadowsocks requires Administrator Mode for transparent system-wide interception."), + DnsPolicyMode.CustomDoh => traffic.RuntimeMode == TrafficRuntimeMode.Admin && traffic.DnsInterceptionActive + ? _context.L("Custom DoH is active for captured applications.") + : traffic.RuntimeMode == TrafficRuntimeMode.Admin && traffic.DnsFailClosedActive + ? _context.L("DNS is blocked because the Custom DoH endpoint is unavailable or invalid.") + : _context.L("Custom DoH requires Administrator Mode for transparent system-wide interception."), + DnsPolicyMode.DnsCrypt => traffic.RuntimeMode == TrafficRuntimeMode.Admin && traffic.DnsInterceptionActive + ? _context.L("Transparent DNS interception is active for captured applications.") + : traffic.RuntimeMode == TrafficRuntimeMode.Admin && traffic.DnsFailClosedActive + ? _context.L("DNS is blocked (fail-closed) because DNSCrypt is unavailable.") + : traffic.RuntimeMode == TrafficRuntimeMode.Game + ? status.Runtime.IsServing + ? _context.L("DNSCrypt is running, but system-wide interception is paused in Game Mode.") + : _context.L("System-wide DNS interception is paused in Game Mode.") + : status.Runtime.IsServing + ? _context.L("DNSCrypt is running in User Mode; system-wide interception requires Administrator Mode.") + : _context.L("DNSCrypt is selected but its runtime is unavailable."), + _ => _context.L("System DNS is unchanged."), + }; + + DnsCryptConfig dnsCryptConfig = policy.dnsCrypt ?? new DnsCryptConfig(); + IReadOnlyList activeResolvers = status.ActiveResolvers ?? Array.Empty(); + _privacyDnsInterception.Text = traffic.RuntimeMode == TrafficRuntimeMode.Admin + ? traffic.DnsInterceptionActive + ? _context.L("Active") + : traffic.DnsFailClosedActive + ? _context.L("Blocked") + : _context.L("Inactive") + : _context.L("Administrator Mode required"); + _privacyUpstream.Text = activeResolvers.Count > 0 + ? string.Join(", ", activeResolvers.Select(resolver => resolver.Name)) + : "—"; + string[] transports = activeResolvers + .Select(resolver => resolver.Protocol?.Trim()) + .OfType() + .Where(protocol => !string.IsNullOrWhiteSpace(protocol)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + _privacyTransport.Text = transports.Length > 0 ? string.Join(", ", transports) : "—"; + _privacyFallback.Text = dnsCryptConfig.failClosed ? _context.L("Blocked") : _context.L("Allowed"); + _privacyBootstrap.Text = status.Mode == DnsPolicyMode.DnsCrypt + ? _context.L("Disabled in DNS runtime") + : "—"; + _privacyRoute.Text = dnsCryptConfig.routeThroughShadowsocks + ? _context.L("Through Shadowsocks") + : _context.L("Direct from DNSCrypt Proxy"); + _privacySelfTest.Text = _privacySelfTestMessage ?? _context.L("Not run"); + + if (!_busy && !string.IsNullOrWhiteSpace(status.LastMaintenanceError)) + { + _progressText.Text = _context.LF("Automatic DNSCrypt update failed: {0}", status.LastMaintenanceError); + } + } + + private void ApplyConfigurationState(DnsCryptConfig config) + { + if (_settingsDirty) + return; + + _autoUpdate.IsOn = config.autoUpdate; + _requireDnssec.IsOn = config.requireDnssec; + _requireNoLog.IsOn = config.requireNoLog; + _requireNoFilter.IsOn = config.requireNoFilter; + _ipv6Servers.IsOn = config.ipv6Servers; + _routeThroughShadowsocks.IsOn = config.routeThroughShadowsocks; + + bool automaticResolvers = config.automaticResolvers; + _resolverAutomatic.IsChecked = automaticResolvers; + _resolverSelected.IsChecked = !automaticResolvers; + _manualResolverNames.Clear(); + foreach (string name in config.serverNames ?? []) + { + if (!string.IsNullOrWhiteSpace(name)) + _manualResolverNames.Add(name.Trim()); + } + EnsureConfiguredResolversVisible(_manualResolverNames); + RebuildResolverList(); + } + + private void MergeActiveResolverData(DnsCryptManagementStatus status) + { + IReadOnlyList active = status.ActiveResolvers ?? Array.Empty(); + if (active.Count == 0) + return; + + var activeByName = active + .Where(resolver => !string.IsNullOrWhiteSpace(resolver.Name)) + .GroupBy(resolver => resolver.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); + + if (_resolverCatalog.Length == 0) + { + _resolverCatalog = active.ToArray(); + return; + } + + _resolverCatalog = _resolverCatalog + .Select(resolver => activeByName.TryGetValue(resolver.Name, out DnsCryptResolverInfo? activeResolver) + ? resolver with + { + Protocol = string.IsNullOrWhiteSpace(activeResolver.Protocol) ? resolver.Protocol : activeResolver.Protocol, + CountryCode = string.IsNullOrWhiteSpace(activeResolver.CountryCode) ? resolver.CountryCode : activeResolver.CountryCode, + CountryName = string.IsNullOrWhiteSpace(activeResolver.CountryName) ? resolver.CountryName : activeResolver.CountryName, + FlagEmoji = string.IsNullOrWhiteSpace(activeResolver.FlagEmoji) ? resolver.FlagEmoji : activeResolver.FlagEmoji, + LatencyMs = activeResolver.LatencyMs ?? resolver.LatencyMs, + } + : resolver) + .ToArray(); + } + + private void ApplyResolverPresentation(DnsCryptManagementStatus status) + { + _automaticResolverList.Items.Clear(); + bool pendingAutomaticApply = _settingsDirty + && _resolverAutomatic.IsChecked == true + && !status.AutomaticResolvers; + IReadOnlyList active = pendingAutomaticApply + ? Array.Empty() + : status.ActiveResolvers ?? Array.Empty(); + foreach (DnsCryptResolverInfo resolver in active) + _automaticResolverList.Items.Add(new ResolverChoice(resolver)); + + bool metadataMissing = active.Any(resolver => + string.IsNullOrWhiteSpace(resolver.Protocol) + || (string.IsNullOrWhiteSpace(resolver.CountryCode) && string.IsNullOrWhiteSpace(resolver.CountryName))); + if (status.AutomaticResolvers + && status.Component.IsInstalled + && active.Count > 0 + && metadataMissing + && !_resolverCatalogLoaded + && !_resolverCatalogLoading) + { + _ = LoadResolversInBackgroundAsync(); + } + + if (pendingAutomaticApply) + { + _automaticResolverStatus.Text = _context.L("Applying DNS settings"); + } + else if (active.Count > 0 && status.Runtime.IsServing) + { + _automaticResolverStatus.Text = _context.LF( + "Automatic filtered selection is active ({0} resolver(s)).", + active.Count); + } + else if (status.AutomaticResolvers && active.Count > 0) + { + _automaticResolverStatus.Text = _context.L( + "Automatic filtered resolver is starting."); + } + else + { + _automaticResolverStatus.Text = _context.L( + "Automatic mode selects a resolver from the signed catalog using the DNSSEC, no-log, unfiltered and address-family settings above."); + } + + UpdateResolverSelectionVisibility(); + } + + private void ApplyControlState(DnsCryptManagementStatus status, TrafficCaptureStatus traffic) + { + bool installed = status.Component.IsInstalled; + bool dnsCryptSelected = status.Mode == DnsPolicyMode.DnsCrypt; + + _installButton.Visibility = installed ? Visibility.Collapsed : Visibility.Visible; + _installButton.IsEnabled = !installed && !_busy; + _checkButton.IsEnabled = installed && !_busy; + _updateButton.IsEnabled = installed && status.UpdateAvailable && !_busy; + _restartButton.IsEnabled = installed + && dnsCryptSelected + && status.Runtime.State is not DnsCryptRuntimeState.Starting and not DnsCryptRuntimeState.Updating + && !_busy; + _testButton.IsEnabled = installed && status.Runtime.IsServing && !_busy; + _privacyTestButton.IsEnabled = installed && dnsCryptSelected && !_busy; + _reinstallButton.IsEnabled = installed && !_busy; + _removeButton.IsEnabled = installed && !_busy; + _dnsCryptMode.IsEnabled = !_busy; + _systemMode.IsEnabled = !_busy; + _directMode.IsEnabled = !_busy; + _proxyMode.IsEnabled = !_busy; + _customDohMode.IsEnabled = !_busy; + _directDnsProvider.IsEnabled = !_busy; + _directDnsPrimaryServer.IsEnabled = !_busy; + _directDnsFallbackServer.IsEnabled = !_busy; + _directDnsRouteThroughShadowsocks.IsEnabled = !_busy; + _customDohUrl.IsEnabled = !_busy; + _customDohRouteThroughShadowsocks.IsEnabled = !_busy; + _autoUpdate.IsEnabled = !_busy; + _requireDnssec.IsEnabled = !_busy; + _requireNoLog.IsEnabled = !_busy; + _requireNoFilter.IsEnabled = !_busy; + _ipv6Servers.IsEnabled = !_busy; + _routeThroughShadowsocks.IsEnabled = !_busy; + _resolverAutomatic.IsEnabled = !_busy; + _resolverSelected.IsEnabled = !_busy; + _loadResolversButton.IsEnabled = installed && !_busy; + _resolverSearchBox.IsEnabled = !_busy; + _resolverProtocolFilter.IsEnabled = !_busy; + _resolverCountryFilter.IsEnabled = !_busy; + _resolverAddressFamilyFilter.IsEnabled = !_busy; + _resolverDnssecFilter.IsEnabled = !_busy; + _resolverNoLogFilter.IsEnabled = !_busy; + _resolverNoFilterFilter.IsEnabled = !_busy; + _resolverList.IsEnabled = !_busy && _resolverSelected.IsChecked == true; + UpdateResolverSelectionVisibility(); + } + + private static RadioButton CreateMode(string text) => new() + { + Content = text, + GroupName = "DnsMode", + }; + + private static Button CreateButton(string text, Func action) + { + var button = new Button { Content = text }; + button.Click += async (_, _) => await action(); + return button; + } + + private static ToggleSwitch CreateToggle(string header) => new() + { + Header = header, + }; + + private void OnResolverSelectionChanged(object sender, RoutedEventArgs e) + { + UpdateResolverSelectionVisibility(); + if (_refreshing || _busy) + return; + + _settingsDirty = true; + _resolverList.IsEnabled = _resolverSelected.IsChecked == true; + if (_resolverSelected.IsChecked == true) + { + if (!_resolverCatalogLoaded) + _ = LoadResolversInBackgroundAsync(); + else + StartResolverLatencyRefresh(); + + if (_manualResolverNames.Count == 0) + { + _resolverListStatus.Text = _context.L("Select at least one resolver."); + return; + } + } + ScheduleDnsCryptSettingsApply(); + } + + private void OnDnsCryptSettingChanged(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy) + return; + + _settingsDirty = true; + ScheduleDnsCryptSettingsApply(); + } + + private void OnResolverListSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_refreshing || _updatingResolverList || _busy) + return; + + foreach (object item in e.AddedItems) + { + if (item is ResolverChoice choice) + _manualResolverNames.Add(choice.Name); + } + foreach (object item in e.RemovedItems) + { + if (item is ResolverChoice choice) + _manualResolverNames.Remove(choice.Name); + } + + _settingsDirty = true; + if (_manualResolverNames.Count == 0) + { + _resolverListStatus.Text = _context.L("Select at least one resolver."); + return; + } + ScheduleDnsCryptSettingsApply(); + } + + private void UpdateResolverSelectionVisibility() + { + bool manual = _resolverSelected.IsChecked == true; + _manualResolverPanel.Visibility = manual ? Visibility.Visible : Visibility.Collapsed; + _automaticResolverPanel.Visibility = manual ? Visibility.Collapsed : Visibility.Visible; + if (!manual) + { + _cancelResolverLatency?.Invoke(); + _cancelResolverLatency = null; + } + } + + + private void OnDirectDnsTextChanged(object sender, TextChangedEventArgs e) + { + if (_refreshing || _updatingDirectDnsPreset) + return; + + _directDnsDirty = true; + SyncDirectDnsPresetFromAddresses(_directDnsPrimaryServer.Text, _directDnsFallbackServer.Text); + } + + private void SyncDirectDnsPresetFromAddresses(string primary, string fallback) + { + string normalizedPrimary = NormalizeDnsAddressForComparison(primary); + string normalizedFallback = NormalizeDnsAddressForComparison(fallback); + DnsServerPreset? match = _dnsServerPresets.FirstOrDefault(preset => + !preset.IsCustom + && string.Equals(NormalizeDnsAddressForComparison(preset.Primary), normalizedPrimary, StringComparison.OrdinalIgnoreCase) + && string.Equals(NormalizeDnsAddressForComparison(preset.Fallback), normalizedFallback, StringComparison.OrdinalIgnoreCase)); + DnsServerPreset selected = match ?? _dnsServerPresets.First(static preset => preset.IsCustom); + + _updatingDirectDnsPreset = true; + try + { + _directDnsProvider.SelectedItem = selected; + _directDnsPrimaryServer.IsReadOnly = !selected.IsCustom; + _directDnsFallbackServer.IsReadOnly = !selected.IsCustom; + } + finally + { + _updatingDirectDnsPreset = false; + } + } + + private async void OnDirectDnsProviderChanged(object sender, SelectionChangedEventArgs e) + { + if (_refreshing || _updatingDirectDnsPreset || _busy || _context.Controller is null + || _directDnsProvider.SelectedItem is not DnsServerPreset preset) + { + return; + } + + _updatingDirectDnsPreset = true; + try + { + _directDnsPrimaryServer.IsReadOnly = !preset.IsCustom; + _directDnsFallbackServer.IsReadOnly = !preset.IsCustom; + if (preset.IsCustom) + { + _directDnsDirty = true; + _directDnsPrimaryServer.Focus(FocusState.Programmatic); + return; + } + + _directDnsPrimaryServer.Text = preset.Primary; + _directDnsFallbackServer.Text = preset.Fallback; + _directDnsDirty = _directMode.IsChecked == true; + } + finally + { + _updatingDirectDnsPreset = false; + } + + if (_directMode.IsChecked == true) + await ApplyDirectDnsServersAsync(preset.Primary, preset.Fallback); + } + + private static string NormalizeDnsAddressForComparison(string value) + => value?.Trim().TrimStart('[').TrimEnd(']') ?? string.Empty; + + private void SyncDohPresetFromUrl(string url) + { + string normalized = url?.Trim() ?? string.Empty; + DohPreset? match = _dohPresets.FirstOrDefault(preset => + !preset.IsCustom && string.Equals(preset.Url, normalized, StringComparison.OrdinalIgnoreCase)); + DohPreset selected = match ?? _dohPresets.First(static preset => preset.IsCustom); + + _updatingDohPreset = true; + try + { + _customDohProvider.SelectedItem = selected; + _customDohUrl.IsReadOnly = !selected.IsCustom; + } + finally + { + _updatingDohPreset = false; + } + } + + private async void OnCustomDohProviderChanged(object sender, SelectionChangedEventArgs e) + { + if (_refreshing || _updatingDohPreset || _busy || _context.Controller is null + || _customDohProvider.SelectedItem is not DohPreset preset) + { + return; + } + + _updatingDohPreset = true; + try + { + _customDohUrl.IsReadOnly = !preset.IsCustom; + if (preset.IsCustom) + { + _customDohDirty = true; + _customDohUrl.Focus(FocusState.Programmatic); + return; + } + + _customDohUrl.Text = preset.Url; + _customDohDirty = _customDohMode.IsChecked == true; + } + finally + { + _updatingDohPreset = false; + } + + if (_customDohMode.IsChecked == true) + { + await ApplyCustomDohUrlAsync(preset.Url); + } + } + + private async Task ApplyCustomDohUrlAsync(string url) + { + if (_context.Controller is null) + return; + + if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out Uri? uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + _customDohDirty = true; + _context.ShowInfo("DNS", "Enter a valid HTTPS DoH URL to enable Custom DoH.", InfoBarSeverity.Warning); + return; + } + + bool routeThroughShadowsocks = _customDohRouteThroughShadowsocks.IsOn; + await RunOperationAsync(async cancellationToken => + { + await _context.Controller.SetCustomDohPolicyAsync( + uri.AbsoluteUri, + routeThroughShadowsocks, + cancellationToken); + _customDohDirty = false; + SetProgressText("Custom DoH settings applied."); + }); + } + + private async void OnCustomDohRouteChanged(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _customDohMode.IsChecked != true || _context.Controller is null) + return; + _customDohDirty = true; + await ApplyCustomDohUrlAsync(_customDohUrl.Text); + } + + private async void OnDirectDnsRouteChanged(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _directMode.IsChecked != true || _context.Controller is null) + return; + + _directDnsDirty = true; + await ApplyDirectDnsServersAsync(_directDnsPrimaryServer.Text, _directDnsFallbackServer.Text); + } + + private async void OnDirectDnsServerLostFocus(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || !_directDnsDirty || _directMode.IsChecked != true || _context.Controller is null) + return; + await ApplyDirectDnsServersAsync(_directDnsPrimaryServer.Text, _directDnsFallbackServer.Text); + } + + private async Task ApplyDirectDnsServersAsync(string primaryValue, string fallbackValue) + { + if (_context.Controller is null) + return; + + string primary = NormalizeDnsAddressForComparison(primaryValue); + string fallback = NormalizeDnsAddressForComparison(fallbackValue); + if (primary.Length > 0 && !IPAddress.TryParse(primary, out _)) + { + _directDnsDirty = true; + _context.ShowInfo("DNS", "Enter a valid primary IPv4 or IPv6 DNS server address.", InfoBarSeverity.Warning); + return; + } + if (fallback.Length > 0 && !IPAddress.TryParse(fallback, out _)) + { + _directDnsDirty = true; + _context.ShowInfo("DNS", "Enter a valid fallback IPv4 or IPv6 DNS server address.", InfoBarSeverity.Warning); + return; + } + if (primary.Length == 0 && fallback.Length > 0) + { + _directDnsDirty = true; + _context.ShowInfo("DNS", "Enter a primary DNS server before configuring a fallback server.", InfoBarSeverity.Warning); + return; + } + + bool routeThroughShadowsocks = _directDnsRouteThroughShadowsocks.IsOn; + await RunOperationAsync(async cancellationToken => + { + await _context.Controller.SetDirectDnsPolicyAsync(primary, fallback, routeThroughShadowsocks, cancellationToken); + _directDnsDirty = false; + SetProgressText(primary.Length == 0 ? "Direct DNS enabled." : "DNS server settings applied."); + }); + } + + private async void OnCustomDohUrlLostFocus(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || !_customDohDirty || _customDohMode.IsChecked != true || _context.Controller is null) + return; + + await ApplyCustomDohUrlAsync(_customDohUrl.Text); + } + + private async void OnSystemModeChecked(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _context.Controller is null) + return; + await RunOperationAsync(async cancellationToken => + { + await _context.Controller.SetDnsPolicyAsync(DnsPolicyMode.System, cancellationToken); + SetProgressText("System DNS enabled."); + }); + } + + private async void OnDirectModeChecked(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _context.Controller is null) + return; + ApplyModeSpecificVisibility(DnsPolicyMode.Direct); + await ApplyDirectDnsServersAsync(_directDnsPrimaryServer.Text, _directDnsFallbackServer.Text); + } + + private async void OnProxyModeChecked(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _context.Controller is null) + return; + await RunOperationAsync(async cancellationToken => + { + await _context.Controller.SetDnsPolicyAsync(DnsPolicyMode.Proxy, cancellationToken); + SetProgressText("DNS through Shadowsocks enabled."); + }); + } + + private async void OnCustomDohModeChecked(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _context.Controller is null) + return; + + ApplyModeSpecificVisibility(DnsPolicyMode.CustomDoh); + if (!Uri.TryCreate(_customDohUrl.Text?.Trim(), UriKind.Absolute, out Uri? uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + _customDohDirty = true; + _context.ShowInfo("DNS", "Enter a valid HTTPS DoH URL to enable Custom DoH.", InfoBarSeverity.Warning); + _customDohUrl.Focus(FocusState.Programmatic); + return; + } + + await ApplyCustomDohUrlAsync(uri.AbsoluteUri); + } + + private async void OnDnsCryptModeChecked(object sender, RoutedEventArgs e) + { + if (_refreshing || _busy || _context.Controller is null) + return; + await RequestEnableDnsCryptAsync(); + } + + internal async Task RequestEnableDnsCryptAsync() + { + if (_busy || _context.Controller is null) + return; + + DnsCryptManagementStatus status = _context.Controller.GetDnsCryptManagementStatus(); + if (!status.Component.IsInstalled) + { + ContentDialogResult result = await ShowInstallDialogAsync(); + if (result != ContentDialogResult.Primary) + { + Refresh(); + return; + } + } + + bool requiresInstall = !_context.Controller.GetDnsCryptManagementStatus().Component.IsInstalled; + await RunDnsCryptOperationAsync(async cancellationToken => + { + if (requiresInstall) + await _context.Controller.InstallDnsCryptAsync(CreateProgress(), cancellationToken); + SetProgressText("Preparing signed DNSCrypt resolver catalog through DoH over Shadowsocks"); + await _context.Controller.SetDnsPolicyAsync(DnsPolicyMode.DnsCrypt, cancellationToken); + SetProgressText("DNSCrypt mode selected; runtime is running."); + }, requiresInstall ? "Installing DNSCrypt Proxy" : "Activating DNSCrypt"); + } + + private Task InstallAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + await _context.Controller!.InstallDnsCryptAsync(CreateProgress(), cancellationToken); + SetProgressText("DNSCrypt Proxy installed."); + }, "Installing DNSCrypt Proxy"); + + private Task CheckUpdateAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + SetProgressText("Checking latest version"); + DnsCryptReleaseInfo release = await _context.Controller!.CheckDnsCryptUpdateAsync(cancellationToken); + DnsCryptManagementStatus status = _context.Controller.GetDnsCryptManagementStatus(); + SetProgressText(status.UpdateAvailable + ? _context.LF("DNSCrypt Proxy {0} is available.", release.Version) + : _context.L("DNSCrypt Proxy is up to date.")); + }, "Checking latest version"); + + private Task UpdateAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + await _context.Controller!.UpdateDnsCryptAsync(CreateProgress(), cancellationToken); + SetProgressText("DNSCrypt Proxy updated."); + }, "Updating DNSCrypt Proxy"); + + private Task RestartAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + SetProgressText("Starting DNSCrypt"); + await _context.Controller!.RestartDnsCryptAsync(cancellationToken); + SetProgressText("DNSCrypt Proxy restarted."); + }, "Starting DNSCrypt"); + + private Task TestDnsCryptAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + SetProgressText("Testing DNSCrypt"); + bool healthy = await _context.Controller!.TestDnsCryptAsync(cancellationToken); + if (!healthy) + throw new InvalidOperationException(_context.L("DNSCrypt test failed.")); + SetProgressText("DNSCrypt test passed."); + }, "Testing DNSCrypt"); + + private Task TestDnsPrivacyAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + SetProgressText("Testing DNS privacy"); + DnsPrivacySelfTestResult result = await _context.Controller!.TestDnsPrivacyAsync(cancellationToken); + if (result.Passed) + { + _privacySelfTestMessage = _context.L("Passed"); + SetProgressText("DNS privacy self-test passed."); + _context.ShowInfo( + "DNS privacy", + _context.LF("Protected DNS path verified: {0} over {1}.", result.Upstream, result.Transport), + InfoBarSeverity.Success); + return; + } + + string failure = result.Failures.Count > 0 + ? string.Join(" ", result.Failures.Select(_context.L)) + : _context.L("Unknown DNS privacy failure."); + _privacySelfTestMessage = _context.LF("Failed: {0}", failure); + SetProgressText(_context.LF("DNS privacy self-test failed: {0}", failure)); + _context.ShowInfo( + "DNS privacy", + _context.LF("DNS privacy self-test failed: {0}", failure), + InfoBarSeverity.Error); + }, "Testing DNS privacy"); + + private Task ReinstallAsync() => RunDnsCryptOperationAsync(async cancellationToken => + { + await _context.Controller!.ReinstallDnsCryptAsync(CreateProgress(), cancellationToken); + SetProgressText("DNSCrypt Proxy reinstalled."); + }, "Installing DNSCrypt Proxy"); + + private async Task RemoveAsync() + { + if (_context.Controller is null) + return; + ContentDialogResult result = await ShowRemoveDialogAsync(); + if (result != ContentDialogResult.Primary) + return; + await RunDnsCryptOperationAsync(async cancellationToken => + { + await _context.Controller.RemoveDnsCryptAsync(CreateProgress(), cancellationToken); + SetProgressText("DNSCrypt Proxy removed."); + }, "Removing DNSCrypt Proxy"); + } + + private void ScheduleDnsCryptSettingsApply() + { + if (_disposed) + return; + + _cancelSettingsApply?.Invoke(); + long generation = ++_settingsApplyGeneration; + _ = ApplyDnsCryptSettingsDebouncedAsync(generation); + } + + private async Task ApplyDnsCryptSettingsDebouncedAsync(long generation) + { + using var cancellation = new CancellationTokenSource(); + Action cancel = cancellation.Cancel; + _cancelSettingsApply = cancel; + CancellationToken cancellationToken = cancellation.Token; + try + { + await Task.Delay(450, cancellationToken); + if (_context.Controller is null || generation != _settingsApplyGeneration) + return; + + DnsCryptConfig config = BuildDnsCryptConfigFromControls(); + if (_resolverSelected.IsChecked == true && config.serverNames.Count == 0) + { + _resolverListStatus.Text = _context.L("Select at least one resolver."); + return; + } + + SetProgressText("Applying DNS settings"); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(28)); + await _context.Controller.SaveDnsCryptSettingsAsync(config, timeout.Token); + if (generation != _settingsApplyGeneration) + return; + + _settingsDirty = false; + SetProgressText("DNS settings applied."); + Refresh(); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + catch (OperationCanceledException) + { + SetProgressText("DNS settings apply timed out; previous runtime was restored."); + _resolverListStatus.Text = _context.L("The selected resolver did not become reachable in time. Previous DNSCrypt settings remain active."); + } + catch (Exception exception) + { + SetProgressText(_context.LF("Failed: {0}", exception.Message)); + _resolverListStatus.Text = _context.LF("Resolver settings were not applied: {0}", exception.Message); + _context.ShowInfo( + "DNSCrypt Proxy", + _context.LF("DNSCrypt operation failed: {0}", exception.Message), + InfoBarSeverity.Error); + } + finally + { + if (ReferenceEquals(_cancelSettingsApply, cancel)) + _cancelSettingsApply = null; + } + } + + private bool IsManualResolverSupported(string name) + { + if (!_resolverCatalogLoaded) + return true; + + DnsCryptResolverInfo? resolver = _resolverCatalog.FirstOrDefault(item => + string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)); + return resolver is not null && IsSupportedResolverProtocol(resolver); + } + + private DnsCryptConfig BuildDnsCryptConfigFromControls() + { + return new DnsCryptConfig + { + autoUpdate = _autoUpdate.IsOn, + requireDnssec = _requireDnssec.IsOn, + requireNoLog = _requireNoLog.IsOn, + requireNoFilter = _requireNoFilter.IsOn, + ipv4Servers = true, + ipv6Servers = _ipv6Servers.IsOn, + routeThroughShadowsocks = _routeThroughShadowsocks.IsOn, + automaticResolvers = _resolverAutomatic.IsChecked == true, + // Fail Closed is an internal compatibility field, not a UI toggle. Preserve + // the persisted value while Administrator-mode DNSCrypt capture enforces its + // own effective fail-closed security invariant. + failClosed = _context.Controller?.GetCurrentConfiguration().dnsPolicy?.dnsCrypt?.failClosed ?? true, + serverNames = _resolverSelected.IsChecked == true + ? _manualResolverNames + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Where(IsManualResolverSupported) + .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) + .ToList() + : [], + }; + } + + private async Task RunDnsCryptOperationAsync( + Func operation, + string initialStatus) + { + if (_busy || _context.Controller is null) + return; + + _dnsCryptOperationActive = true; + SetProgressText(initialStatus); + try + { + await RunOperationAsync(operation); + } + finally + { + _dnsCryptOperationActive = false; + Refresh(); + } + } + + private async Task RunOperationAsync(Func operation) + { + if (_busy || _context.Controller is null) + return; + + _busy = true; + using var cancellation = new CancellationTokenSource(); + Action cancel = cancellation.Cancel; + _cancelOperation = cancel; + _cancelButton.IsEnabled = true; + _progressRing.IsActive = true; + _progressBar.IsIndeterminate = true; + Refresh(); + try + { + await operation(cancellation.Token); + } + catch (OperationCanceledException) + { + SetProgressText("Operation cancelled."); + } + catch (Exception exception) + { + bool dnsCryptContext = _dnsCryptMode.IsChecked == true; + string failure = FormatOperationFailure(exception, dnsCryptContext); + SetProgressText(_context.LF("Failed: {0}", failure)); + _context.ShowInfo( + dnsCryptContext ? "DNSCrypt Proxy" : "DNS", + failure, + InfoBarSeverity.Error); + } + finally + { + if (ReferenceEquals(_cancelOperation, cancel)) + _cancelOperation = null; + _busy = false; + _cancelButton.IsEnabled = false; + _progressRing.IsActive = false; + _progressBar.IsIndeterminate = false; + Refresh(); + } + } + + private string FormatOperationFailure(Exception exception, bool dnsCryptContext) + { + if (exception is DnsCryptComponentNetworkException) + { + return _context.L( + "DNSCrypt could not reach GitHub through Cloudflare DoH over Shadowsocks or the Google DoH fallback. Verify that a Shadowsocks server is connected and HTTPS through the proxy works, then try again. No DNSCrypt component changes were applied."); + } + + if (exception is DnsCryptBootstrapException) + { + return _context.L( + "DNSCrypt could not refresh the signed resolver catalog through Cloudflare DoH over Shadowsocks or the Google DoH fallback. Verify that a Shadowsocks server is connected and HTTPS through the proxy works, then try again. The previous DNS mode was kept."); + } + + if (exception is DnsCryptRuntimeStartupException) + { + return _context.L( + "DNSCrypt Proxy could not become ready. Open Logs to see the upstream error, then try again. The previous DNS mode was kept."); + } + + return dnsCryptContext + ? _context.LF("DNSCrypt operation failed: {0}", exception.Message) + : _context.LF("DNS operation failed: {0}", exception.Message); + } + + private Progress CreateProgress() => new(progress => + { + string stage = progress.Stage switch + { + DnsCryptComponentStage.CheckingRelease => _context.L("Checking latest version"), + DnsCryptComponentStage.DownloadingArchive => _context.L("Downloading DNSCrypt Proxy"), + DnsCryptComponentStage.DownloadingSignature => _context.L("Downloading signature"), + DnsCryptComponentStage.Verifying => _context.L("Verifying signature"), + DnsCryptComponentStage.Installing => _context.L("Installing"), + DnsCryptComponentStage.Prepared => _context.L("Prepared"), + DnsCryptComponentStage.ValidatingRuntime => _context.L("Validating DNSCrypt runtime"), + DnsCryptComponentStage.Activating => _context.L("Activating"), + DnsCryptComponentStage.Starting => _context.L("Starting DNSCrypt"), + DnsCryptComponentStage.Removing => _context.L("Removing"), + _ => _context.L("Ready"), + }; + + if (progress.TotalBytes is > 0) + { + double percent = Math.Clamp(progress.BytesTransferred * 100d / progress.TotalBytes.Value, 0d, 100d); + _progressBar.IsIndeterminate = false; + _progressBar.Value = percent; + _progressText.Text = $"{stage} · {FormatBytes(progress.BytesTransferred)} / {FormatBytes(progress.TotalBytes.Value)}"; + } + else + { + _progressBar.IsIndeterminate = progress.Stage is not (DnsCryptComponentStage.Ready or DnsCryptComponentStage.Prepared); + _progressText.Text = stage; + } + }); + + private async Task ShowInstallDialogAsync() + { + var dialog = new ContentDialog + { + XamlRoot = _context.GetXamlRoot(), + Title = _context.L("DNSCrypt Proxy is not installed."), + Content = new TextBlock + { + Text = _context.L("Shadowsocks can download and verify the official Windows x64 release from DNSCrypt/dnscrypt-proxy."), + TextWrapping = TextWrapping.Wrap, + }, + PrimaryButtonText = _context.L("Download and enable"), + CloseButtonText = _context.L("Cancel"), + DefaultButton = ContentDialogButton.Primary, + }; + return await dialog.ShowAsync(); + } + + private async Task ShowRemoveDialogAsync() + { + var dialog = new ContentDialog + { + XamlRoot = _context.GetXamlRoot(), + Title = _context.L("Remove DNSCrypt Proxy"), + Content = new TextBlock + { + Text = _context.L("DNS mode will switch to System DNS and the downloaded DNSCrypt component will be removed."), + TextWrapping = TextWrapping.Wrap, + }, + PrimaryButtonText = _context.L("Remove"), + CloseButtonText = _context.L("Cancel"), + DefaultButton = ContentDialogButton.Close, + }; + return await dialog.ShowAsync(); + } + + private void SetProgressText(string keyOrLocalizedText) + { + string localized = _context.L(keyOrLocalizedText); + _progressText.Text = localized; + } + + private Task LoadResolversAsync() => RunOperationAsync(async cancellationToken => + { + SetProgressText("Loading resolver list"); + IReadOnlyList resolvers = await _context.Controller!.GetDnsCryptResolversAsync(cancellationToken); + _resolverCatalog = resolvers.Where(IsSupportedResolverProtocol).ToArray(); + _resolverCatalogLoaded = true; + EnsureConfiguredResolversVisible(_manualResolverNames); + RefreshResolverCountryFilter(); + RebuildResolverList(); + _resolverListStatus.Text = _context.LF("Loaded {0} upstream resolvers. Measuring latency in the background.", _resolverCatalog.Length); + SetProgressText("Resolver list loaded."); + StartResolverLatencyRefresh(); + }); + + private async Task LoadResolversInBackgroundAsync() + { + if (_context.Controller is null || _resolverCatalogLoaded || _resolverCatalogLoading) + return; + + _resolverCatalogLoading = true; + try + { + _resolverListStatus.Text = _context.L("Loading resolver list"); + IReadOnlyList resolvers = await _context.Controller.GetDnsCryptResolversAsync(); + _resolverCatalog = resolvers.Where(IsSupportedResolverProtocol).ToArray(); + _resolverCatalogLoaded = true; + EnsureConfiguredResolversVisible(_manualResolverNames); + RefreshResolverCountryFilter(); + RebuildResolverList(); + _resolverListStatus.Text = _context.LF("Loaded {0} upstream resolvers. Measuring latency in the background.", _resolverCatalog.Length); + if (_resolverSelected.IsChecked == true) + StartResolverLatencyRefresh(); + else + Refresh(); + } + catch (Exception exception) + { + _resolverListStatus.Text = _context.LF("Failed: {0}", exception.Message); + } + finally + { + _resolverCatalogLoading = false; + } + } + + private void StartResolverLatencyRefresh() + { + if (!_resolverCatalogLoaded || _context.Controller is null || _resolverCatalog.Length == 0) + return; + + _cancelResolverLatency?.Invoke(); + _ = RefreshResolverLatenciesInBackgroundAsync(); + } + + private async Task RefreshResolverLatenciesInBackgroundAsync() + { + using var cancellation = new CancellationTokenSource(); + Action cancel = cancellation.Cancel; + _cancelResolverLatency = cancel; + CancellationToken cancellationToken = cancellation.Token; + try + { + string[] unresolved = _resolverCatalog + .Where(IsResolverProtocolVisible) + .Where(resolver => !resolver.LatencyMs.HasValue) + .OrderBy(resolver => _manualResolverNames.Contains(resolver.Name) ? 0 : 1) + .ThenBy(resolver => resolver.CountryCode, StringComparer.OrdinalIgnoreCase) + .ThenBy(resolver => resolver.Name, StringComparer.OrdinalIgnoreCase) + .Select(resolver => resolver.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + const int batchSize = 80; + for (int offset = 0; offset < unresolved.Length; offset += batchSize) + { + cancellationToken.ThrowIfCancellationRequested(); + string[] batch = unresolved.Skip(offset).Take(batchSize).ToArray(); + IReadOnlyDictionary measured = await _context.Controller! + .ProbeDnsCryptResolverLatenciesAsync(batch, cancellationToken); + if (measured.Count > 0) + { + _resolverCatalog = _resolverCatalog.Select(resolver => measured.TryGetValue(resolver.Name, out int latencyMs) + ? resolver with { LatencyMs = latencyMs } + : resolver).ToArray(); + RebuildResolverList(); + } + + int measuredCount = _resolverCatalog.Count(resolver => resolver.LatencyMs.HasValue); + _resolverListStatus.Text = _context.LF( + "Latency available for {0} of {1} resolver(s).", + measuredCount, + _resolverCatalog.Length); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _resolverListStatus.Text = _context.LF("Resolver latency measurement failed: {0}", exception.Message); + } + finally + { + if (ReferenceEquals(_cancelResolverLatency, cancel)) + _cancelResolverLatency = null; + } + } + + private void RefreshResolverCountryFilter() + { + if (_resolverCountryFilter is null) + return; + + string selectedCode = (_resolverCountryFilter.SelectedItem as ResolverCountryFilter)?.CountryCode ?? string.Empty; + var choices = new List + { + new(string.Empty, _context.L("All countries")), + }; + choices.AddRange(_resolverCatalog + .Where(IsResolverProtocolVisible) + .Where(resolver => !string.IsNullOrWhiteSpace(resolver.CountryCode)) + .GroupBy(resolver => resolver.CountryCode, StringComparer.OrdinalIgnoreCase) + .Select(group => + { + DnsCryptResolverInfo resolver = group.First(); + string flag = string.IsNullOrWhiteSpace(resolver.FlagEmoji) ? "•" : resolver.FlagEmoji; + string name = string.IsNullOrWhiteSpace(resolver.CountryName) ? resolver.CountryCode : resolver.CountryName; + return new ResolverCountryFilter(group.Key, $"{flag} {name}"); + }) + .OrderBy(choice => choice.Label, StringComparer.CurrentCultureIgnoreCase)); + + _resolverCountryFilter.ItemsSource = choices; + _resolverCountryFilter.SelectedItem = choices.FirstOrDefault(choice => + string.Equals(choice.CountryCode, selectedCode, StringComparison.OrdinalIgnoreCase)) ?? choices[0]; + } + + private void EnsureConfiguredResolversVisible(IEnumerable configuredNames) + { + string[] configured = configuredNames + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + if (configured.Length == 0) + return; + + var known = _resolverCatalog + .Select(resolver => resolver.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + DnsCryptResolverInfo[] missing = configured + .Where(name => !known.Contains(name)) + .Select(name => new DnsCryptResolverInfo( + name, string.Empty, false, null, false, false, string.Empty, Array.Empty())) + .ToArray(); + if (missing.Length > 0) + _resolverCatalog = _resolverCatalog.Concat(missing).ToArray(); + } + + private void RebuildResolverList() + { + if (_resolverList is null || _resolverSearchBox is null) + return; + + string query = _resolverSearchBox.Text?.Trim() ?? string.Empty; + IEnumerable filtered = _resolverCatalog.Where(IsResolverProtocolVisible); + if (query.Length > 0) + { + filtered = filtered.Where(resolver => + Contains(resolver.Name, query) + || Contains(resolver.CountryCode, query) + || Contains(resolver.CountryName, query) + || Contains(resolver.Description, query)); + } + + + if (_resolverCountryFilter?.SelectedItem is ResolverCountryFilter country + && !string.IsNullOrWhiteSpace(country.CountryCode)) + { + filtered = filtered.Where(resolver => + string.Equals(resolver.CountryCode, country.CountryCode, StringComparison.OrdinalIgnoreCase)); + } + + if (_resolverAddressFamilyFilter?.SelectedIndex == 1) + filtered = filtered.Where(resolver => !resolver.IPv6); + else if (_resolverAddressFamilyFilter?.SelectedIndex == 2) + filtered = filtered.Where(resolver => resolver.IPv6); + if (_resolverDnssecFilter?.IsOn == true) + filtered = filtered.Where(resolver => resolver.Dnssec == true); + if (_resolverNoLogFilter?.IsOn == true) + filtered = filtered.Where(resolver => resolver.NoLog); + if (_resolverNoFilterFilter?.IsOn == true) + filtered = filtered.Where(resolver => resolver.NoFilter); + + DnsCryptResolverInfo[] visible = filtered + .OrderBy(resolver => resolver.CountryCode, StringComparer.OrdinalIgnoreCase) + .ThenBy(resolver => resolver.LatencyMs ?? int.MaxValue) + .ThenBy(resolver => resolver.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + _updatingResolverList = true; + try + { + _resolverList.Items.Clear(); + foreach (DnsCryptResolverInfo resolver in visible) + { + var choice = new ResolverChoice(resolver); + _resolverList.Items.Add(choice); + if (_manualResolverNames.Contains(resolver.Name)) + _resolverList.SelectedItems.Add(choice); + } + } + finally + { + _updatingResolverList = false; + } + + if (_resolverCatalogLoaded && query.Length > 0) + _resolverListStatus.Text = _context.LF("Showing {0} matching resolver(s).", visible.Length); + } + + private bool IsResolverProtocolVisible(DnsCryptResolverInfo resolver) + { + if (!IsSupportedResolverProtocol(resolver)) + return false; + + string selectedProtocol = (_resolverProtocolFilter.SelectedItem as ResolverProtocolFilter)?.Protocol ?? string.Empty; + if (string.IsNullOrWhiteSpace(selectedProtocol)) + return true; + return string.Equals(resolver.Protocol?.Trim(), selectedProtocol, StringComparison.OrdinalIgnoreCase) + || (string.Equals(selectedProtocol, "DNSCrypt", StringComparison.OrdinalIgnoreCase) + && Contains(resolver.Protocol, "DNSCrypt")); + } + + private static bool IsSupportedResolverProtocol(DnsCryptResolverInfo resolver) + => resolver is not null + && (Contains(resolver.Protocol, "DNSCrypt") + || string.Equals(resolver.Protocol?.Trim(), "DoH", StringComparison.OrdinalIgnoreCase)); + + private static bool Contains(string? value, string query) + => !string.IsNullOrWhiteSpace(value) + && value.Contains(query, StringComparison.OrdinalIgnoreCase); + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _cancelOperation?.Invoke(); + _cancelSettingsApply?.Invoke(); + _cancelResolverLatency?.Invoke(); + _cancelOperation = null; + _cancelSettingsApply = null; + _cancelResolverLatency = null; + GC.SuppressFinalize(this); + } + + private static string FormatBytes(long bytes) + { + if (bytes < 1024) return $"{bytes} B"; + if (bytes < 1024 * 1024) return $"{bytes / 1024d:0.0} KB"; + return $"{bytes / (1024d * 1024d):0.0} MB"; + } +} diff --git a/Shadowsocks.WinUI/Pages/ForwardProxyPage.cs b/Shadowsocks.WinUI/Pages/ForwardProxyPage.cs new file mode 100644 index 000000000..4a6cf7736 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/ForwardProxyPage.cs @@ -0,0 +1,209 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class ForwardProxyPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly RadioButton _noProxy; + private readonly RadioButton _socks5; + private readonly RadioButton _http; + private readonly TextBox _server; + private readonly NumberBox _port; + private readonly NumberBox _timeout; + private readonly TextBox _username; + private readonly PasswordBox _password; + private readonly StackPanel _proxySettingsPanel; + private bool _refreshing; + + internal ForwardProxyPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Forward Proxy", + "Connect to the Shadowsocks server through an optional upstream proxy.", + out StackPanel panel); + + var card = new StackPanel { Spacing = 16 }; + card.Children.Add(WinUIStyles.CreateSectionTitle("Type")); + + var modes = new StackPanel { Spacing = 6 }; + _noProxy = new RadioButton { Content = "No proxy", GroupName = "ForwardProxyType" }; + _socks5 = new RadioButton { Content = "SOCKS5", GroupName = "ForwardProxyType" }; + _http = new RadioButton { Content = "HTTP", GroupName = "ForwardProxyType" }; + _context.SetToolTip(_noProxy, "Connect to the Shadowsocks server directly, without an upstream proxy."); + _context.SetToolTip(_socks5, "Connect to the Shadowsocks server through an upstream SOCKS5 proxy."); + _context.SetToolTip(_http, "Connect to the Shadowsocks server through an upstream HTTP proxy."); + _noProxy.Checked += (_, _) => UpdateProxySettingsVisibility(); + _socks5.Checked += (_, _) => UpdateProxySettingsVisibility(); + _http.Checked += (_, _) => UpdateProxySettingsVisibility(); + modes.Children.Add(_noProxy); + modes.Children.Add(_socks5); + modes.Children.Add(_http); + card.Children.Add(modes); + + _proxySettingsPanel = new StackPanel { Spacing = 16 }; + _proxySettingsPanel.Children.Add(WinUIStyles.CreateSectionTitle("Details")); + Grid details = CreateFormGrid(); + _server = new TextBox { PlaceholderText = "127.0.0.1" }; + _context.SetToolTip(_server, "Hostname or IP address of the upstream proxy."); + AddRow(details, "Address", _server); + _port = CreateNumberBox(1, 65535, 1080); + _context.SetToolTip(_port, "TCP port of the upstream proxy."); + AddRow(details, "Port", _port); + _timeout = CreateNumberBox(1, ForwardProxyConfig.MaxProxyTimeoutSec, 3); + _context.SetToolTip(_timeout, "Connection timeout for the upstream proxy, in seconds."); + AddRow(details, "Timeout", _timeout); + _proxySettingsPanel.Children.Add(details); + + _proxySettingsPanel.Children.Add(WinUIStyles.CreateSectionTitle("Credentials (optional)")); + Grid credentials = CreateFormGrid(); + _username = new TextBox(); + _context.SetToolTip(_username, "Optional username for upstream proxy authentication."); + AddRow(credentials, "Username", _username); + _password = new PasswordBox { PasswordRevealMode = PasswordRevealMode.Peek }; + _context.SetToolTip(_password, "Optional password for upstream proxy authentication."); + AddRow(credentials, "Password", _password); + _proxySettingsPanel.Children.Add(credentials); + card.Children.Add(_proxySettingsPanel); + + var actions = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 8, + }; + var discard = new Button { Content = "Cancel", MinWidth = 92 }; + _context.SetToolTip(discard, "Discard unsaved forward-proxy changes."); + discard.Click += (_, _) => Refresh(); + actions.Children.Add(discard); + var save = new Button { Content = "Save", MinWidth = 92 }; + _context.SetToolTip(save, "Validate and save the forward-proxy configuration."); + save.Click += OnSaveClicked; + actions.Children.Add(save); + card.Children.Add(actions); + + panel.Children.Add(WinUIStyles.CreateCard(card)); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + ForwardProxyConfig? proxy = _context.Controller?.GetCurrentConfiguration().proxy; + if (proxy is null) + { + return; + } + + _refreshing = true; + try + { + _noProxy.IsChecked = !proxy.useProxy; + _http.IsChecked = proxy.useProxy && proxy.proxyType == ForwardProxyConfig.ProxyHttp; + _socks5.IsChecked = proxy.useProxy && proxy.proxyType != ForwardProxyConfig.ProxyHttp; + _server.Text = proxy.proxyServer ?? string.Empty; + _port.Value = proxy.proxyPort > 0 ? proxy.proxyPort : 1080; + _timeout.Value = proxy.proxyTimeout > 0 ? proxy.proxyTimeout : 3; + _username.Text = proxy.authUser ?? string.Empty; + _password.Password = proxy.authPwd ?? string.Empty; + UpdateProxySettingsVisibility(); + } + finally + { + _refreshing = false; + } + } + + private void UpdateProxySettingsVisibility() + => _proxySettingsPanel.Visibility = _noProxy.IsChecked == true + ? Visibility.Collapsed + : Visibility.Visible; + + private void OnSaveClicked(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + bool useProxy = _noProxy.IsChecked != true; + if (useProxy && string.IsNullOrWhiteSpace(_server.Text)) + { + _context.ShowInfo("Forward Proxy", "Proxy address is required.", InfoBarSeverity.Error); + return; + } + + int port = double.IsNaN(_port.Value) ? 0 : checked((int)_port.Value); + int timeout = double.IsNaN(_timeout.Value) ? 0 : checked((int)_timeout.Value); + if (useProxy && (port is < 1 or > 65535)) + { + _context.ShowInfo("Forward Proxy", "Proxy port must be between 1 and 65535.", InfoBarSeverity.Error); + return; + } + if (useProxy && (timeout is < 1 or > ForwardProxyConfig.MaxProxyTimeoutSec)) + { + _context.ShowInfo("Forward Proxy", _context.LF("Timeout must be between 1 and {0} seconds.", ForwardProxyConfig.MaxProxyTimeoutSec), InfoBarSeverity.Error); + return; + } + + string username = _username.Text?.Trim() ?? string.Empty; + string password = _password.Password ?? string.Empty; + if (string.IsNullOrWhiteSpace(username) != string.IsNullOrWhiteSpace(password)) + { + _context.ShowInfo("Forward Proxy", "Username and password must be specified together.", InfoBarSeverity.Error); + return; + } + + _context.Controller.SaveProxy(new ForwardProxyConfig + { + useProxy = useProxy, + proxyType = _http.IsChecked == true ? ForwardProxyConfig.ProxyHttp : ForwardProxyConfig.ProxySocks5, + proxyServer = _server.Text?.Trim() ?? string.Empty, + proxyPort = useProxy ? port : 0, + proxyTimeout = useProxy ? timeout : 3, + useAuth = useProxy && !string.IsNullOrWhiteSpace(username), + authUser = username, + authPwd = password, + }); + _context.ShowInfo("Forward Proxy", "Forward proxy settings saved.", InfoBarSeverity.Success); + } + + private static Grid CreateFormGrid() + { + var grid = new Grid { ColumnSpacing = 14, RowSpacing = 8 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(140) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + return grid; + } + + private static void AddRow(Grid grid, string label, FrameworkElement control) + { + int row = grid.RowDefinitions.Count; + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + TextBlock labelBlock = WinUIStyles.CreateText(label); + labelBlock.HorizontalAlignment = HorizontalAlignment.Right; + labelBlock.VerticalAlignment = VerticalAlignment.Center; + Grid.SetRow(labelBlock, row); + grid.Children.Add(labelBlock); + control.HorizontalAlignment = HorizontalAlignment.Stretch; + Grid.SetRow(control, row); + Grid.SetColumn(control, 1); + grid.Children.Add(control); + } + + private static NumberBox CreateNumberBox(double minimum, double maximum, double value) + => new() + { + Minimum = minimum, + Maximum = maximum, + Value = value, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + }; +} diff --git a/Shadowsocks.WinUI/Pages/GamesPage.cs b/Shadowsocks.WinUI/Pages/GamesPage.cs new file mode 100644 index 000000000..07d973017 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/GamesPage.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Controller.Traffic.Applications; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class GamesPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly TextBlock _modeValue; + private readonly TextBlock _triggerValue; + private readonly TextBlock _winDivertValue; + private readonly StackPanel _applications; + private readonly TextBox _newApplication; + private readonly StackPanel _detectedGames; + private readonly TextBlock _scanStatus; + private readonly Button _scanButton; + private readonly ProgressRing _scanProgress; + private readonly Button _addButton; + private readonly Button _saveButton; + private readonly Button _discardButton; + private readonly List _applicationItems = new(); + private IReadOnlyList _discoveredGames = Array.Empty(); + private bool _dirty; + private bool _scanStarted; + private bool _scanRunning; + + internal GamesPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Game compatibility", + "Admin capture is suspended automatically while a configured anti-cheat-sensitive application is running.", + out StackPanel panel); + + var stateCard = new StackPanel { Spacing = 10 }; + stateCard.Children.Add(WinUIStyles.CreateSectionTitle("Current state")); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("Game Mode", out _modeValue)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("Triggered by", out _triggerValue)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("WinDivert", out _winDivertValue)); + panel.Children.Add(WinUIStyles.CreateCard(stateCard)); + + var detectedCard = new StackPanel { Spacing = 12 }; + detectedCard.Children.Add(WinUIStyles.CreateSectionTitle("Detected games")); + detectedCard.Children.Add(WinUIStyles.CreateText( + "Shadowsocks can scan local Steam, Epic Games, GOG and Xbox game libraries. Nothing is added until you choose Add.")); + + var scanActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 }; + _scanButton = new Button { Content = "Scan installed games" }; + _context.SetToolTip(_scanButton, "Scan Steam, Epic Games, GOG and Xbox libraries for installed games that can be added to automatic Game Mode."); + _scanButton.Click += async (_, _) => await ScanInstalledGamesAsync(); + scanActions.Children.Add(_scanButton); + _scanProgress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + VerticalAlignment = VerticalAlignment.Center, + }; + scanActions.Children.Add(_scanProgress); + _scanStatus = WinUIStyles.CreateText("Scan has not run yet.", "CaptionTextBlockStyle"); + _scanStatus.Opacity = 0.72; + scanActions.Children.Add(_scanStatus); + detectedCard.Children.Add(scanActions); + + _detectedGames = new StackPanel { Spacing = 8 }; + detectedCard.Children.Add(_detectedGames); + panel.Children.Add(WinUIStyles.CreateCard(detectedCard)); + + var appCard = new StackPanel { Spacing = 12 }; + appCard.Children.Add(WinUIStyles.CreateSectionTitle("Applications")); + appCard.Children.Add(WinUIStyles.CreateText( + "Use an executable name, full path, or wildcard pattern. Game Mode is automatic and is never a selectable traffic mode.")); + _applications = new StackPanel { Spacing = 8 }; + appCard.Children.Add(_applications); + + _newApplication = new TextBox + { + Header = "Add application manually", + PlaceholderText = "game.exe or C:\\Games\\Game\\game.exe", + }; + _context.SetToolTip(_newApplication, "Enter an executable name, full path, or wildcard pattern to trigger automatic Game Mode."); + _newApplication.TextChanged += (_, _) => UpdateActionState(); + appCard.Children.Add(_newApplication); + + var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _addButton = new Button { Content = "Add", IsEnabled = false }; + _context.SetToolTip(_addButton, "Add the manually entered application pattern to the Game Mode list."); + _addButton.Click += OnAddClicked; + actions.Children.Add(_addButton); + _saveButton = new Button { Content = "Save", IsEnabled = false }; + _context.SetToolTip(_saveButton, "Save the configured Game Mode application list."); + _saveButton.Click += OnSaveClicked; + actions.Children.Add(_saveButton); + _discardButton = new Button { Content = "Discard changes", IsEnabled = false }; + _context.SetToolTip(_discardButton, "Discard unsaved Game Mode application changes."); + _discardButton.Click += (_, _) => + { + SetDirty(false); + Refresh(); + }; + actions.Children.Add(_discardButton); + appCard.Children.Add(actions); + panel.Children.Add(WinUIStyles.CreateCard(appCard)); + + Loaded += async (_, _) => + { + if (!_scanStarted) + { + await ScanInstalledGamesAsync(); + } + }; + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Shadowsocks.Controller.ShadowsocksController? controller = _context.Controller; + if (controller is null) + { + _modeValue.Text = _context.L("Unavailable"); + _triggerValue.Text = "—"; + _winDivertValue.Text = "—"; + return; + } + + TrafficCaptureStatus status = controller.GetTrafficCaptureStatus(); + _modeValue.Text = status.GameModeActive ? _context.L("ACTIVE") : _context.L("Inactive"); + _triggerValue.Text = status.RunningGameApplications.Count > 0 + ? string.Join(", ", status.RunningGameApplications) + : "—"; + _winDivertValue.Text = status.GameModeActive + ? _context.L("Suspended") + : status.WinDivertActive ? _context.L("Active") : _context.L("Inactive"); + + if (!_dirty) + { + Configuration configuration = controller.GetCurrentConfiguration(); + _applicationItems.Clear(); + _applicationItems.AddRange(configuration.gameModeApplications ?? []); + } + RebuildApplications(status.RunningGameApplications); + RebuildDetectedGames(); + UpdateActionState(); + } + + private async Task ScanInstalledGamesAsync() + { + if (_scanRunning) + { + return; + } + + _scanStarted = true; + _scanRunning = true; + _scanButton.IsEnabled = false; + _scanProgress.IsActive = true; + _scanProgress.Visibility = Visibility.Visible; + _scanStatus.Text = _context.L("Scanning installed games…"); + try + { + _discoveredGames = await Task.Run(() => GameDiscoveryService.Discover()); + _scanStatus.Text = _discoveredGames.Count == 0 + ? _context.L("No supported game libraries were detected.") + : _context.LF("Found {0} games.", _discoveredGames.Count); + } + catch (Exception exception) + { + _discoveredGames = Array.Empty(); + _scanStatus.Text = _context.L("Game scan failed."); + _context.ShowInfo("Game compatibility", exception.Message, InfoBarSeverity.Warning); + } + finally + { + _scanRunning = false; + _scanButton.IsEnabled = true; + _scanProgress.IsActive = false; + _scanProgress.Visibility = Visibility.Collapsed; + RebuildDetectedGames(); + } + } + + private void RebuildDetectedGames() + { + _detectedGames.Children.Clear(); + if (_discoveredGames.Count == 0) + { + TextBlock empty = WinUIStyles.CreateText(_context.L("No detected games to show.")); + empty.Opacity = 0.68; + _detectedGames.Children.Add(empty); + return; + } + + foreach (DiscoveredGame game in _discoveredGames) + { + bool alreadyAdded = _applicationItems.Any(existing => + string.Equals(existing, game.ExecutablePath, StringComparison.OrdinalIgnoreCase) + || string.Equals(existing, Path.GetFileName(game.ExecutablePath), StringComparison.OrdinalIgnoreCase)); + + var row = new Grid { ColumnSpacing = 12 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var details = new StackPanel { Spacing = 2 }; + details.Children.Add(WinUIStyles.CreateText(game.Name, "BodyStrongTextBlockStyle")); + TextBlock source = WinUIStyles.CreateText($"{game.Source} · {game.ExecutablePath}", "CaptionTextBlockStyle"); + source.TextTrimming = TextTrimming.CharacterEllipsis; + source.Opacity = 0.66; + details.Children.Add(source); + row.Children.Add(details); + + var add = new Button + { + Content = alreadyAdded ? _context.L("Added") : _context.L("Add"), + IsEnabled = !alreadyAdded, + Tag = game.ExecutablePath, + VerticalAlignment = VerticalAlignment.Center, + }; + _context.SetToolTip(add, "Add this detected game executable to the automatic Game Mode list."); + add.Click += OnAddDetectedGameClicked; + Grid.SetColumn(add, 1); + row.Children.Add(add); + _detectedGames.Children.Add(WinUIStyles.CreateCard(row)); + } + } + + private void RebuildApplications(IReadOnlyList runningApplications) + { + _applications.Children.Clear(); + if (_applicationItems.Count == 0) + { + TextBlock empty = WinUIStyles.CreateText(_context.L("No game applications are configured yet.")); + empty.Opacity = 0.72; + _applications.Children.Add(empty); + return; + } + + for (int index = 0; index < _applicationItems.Count; index++) + { + string application = _applicationItems[index]; + string fileName = Path.GetFileName(application); + bool running = runningApplications.Any(name => + string.Equals(name, fileName, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, application, StringComparison.OrdinalIgnoreCase)); + + var row = new Grid { ColumnSpacing = 12 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var value = new StackPanel { Spacing = 2 }; + value.Children.Add(WinUIStyles.CreateText(application, "BodyStrongTextBlockStyle")); + TextBlock state = WinUIStyles.CreateText(running ? _context.L("Running") : _context.L("Not running"), "CaptionTextBlockStyle"); + state.Opacity = running ? 1 : 0.65; + value.Children.Add(state); + row.Children.Add(value); + + var remove = new Button { Content = _context.L("Remove"), Tag = index }; + _context.SetToolTip(remove, "Remove this application from the automatic Game Mode list."); + remove.Click += OnRemoveClicked; + Grid.SetColumn(remove, 2); + row.Children.Add(remove); + _applications.Children.Add(WinUIStyles.CreateCard(row)); + } + } + + private void OnAddDetectedGameClicked(object sender, RoutedEventArgs _) + { + if (sender is not Button { Tag: string executablePath } || string.IsNullOrWhiteSpace(executablePath)) + { + return; + } + if (!_applicationItems.Any(existing => string.Equals(existing, executablePath, StringComparison.OrdinalIgnoreCase))) + { + _applicationItems.Add(executablePath); + SetDirty(true); + } + Refresh(); + } + + private void OnAddClicked(object _, RoutedEventArgs _1) + { + string value = _newApplication.Text?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (!_applicationItems.Any(existing => string.Equals(existing, value, StringComparison.OrdinalIgnoreCase))) + { + _applicationItems.Add(value); + SetDirty(true); + } + _newApplication.Text = string.Empty; + Refresh(); + } + + private void OnRemoveClicked(object sender, RoutedEventArgs _1) + { + if (sender is not Button { Tag: int index } || index < 0 || index >= _applicationItems.Count) + { + return; + } + + _applicationItems.RemoveAt(index); + SetDirty(true); + Refresh(); + } + + private void SetDirty(bool dirty) + { + _dirty = dirty; + UpdateActionState(); + } + + private void UpdateActionState() + { + string candidate = _newApplication.Text?.Trim() ?? string.Empty; + _addButton.IsEnabled = !string.IsNullOrWhiteSpace(candidate) + && !_applicationItems.Any(existing => string.Equals(existing, candidate, StringComparison.OrdinalIgnoreCase)); + _saveButton.IsEnabled = _dirty; + _discardButton.IsEnabled = _dirty; + } + + private async void OnSaveClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + Configuration configuration = _context.Controller.GetCurrentConfiguration(); + bool saved = await _context.Controller.SaveTrafficRoutingAsync( + configuration.trafficCaptureMode, + configuration.applicationRules ?? [], + _applicationItems); + if (saved) + { + SetDirty(false); + _context.ShowInfo("Game compatibility", "Automatic Game Mode application list saved.", InfoBarSeverity.Success); + } + else + { + _context.ShowInfo("Game compatibility", "The application list could not be applied.", InfoBarSeverity.Warning); + } + Refresh(); + } +} diff --git a/Shadowsocks.WinUI/Pages/HotkeysPage.cs b/Shadowsocks.WinUI/Pages/HotkeysPage.cs new file mode 100644 index 000000000..4de843883 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/HotkeysPage.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using global::Windows.System; +using Shadowsocks.Controller.Hotkeys; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed partial class HotkeysPage : Page, IRefreshablePage +{ + private static readonly (string Key, string Label)[] Definitions = + [ + ("SwitchSystemProxy", "Toggle system proxy"), + ("SwitchSystemProxyMode", "Switch Global / PAC"), + ("SwitchAllowLan", "Allow clients from LAN"), + ("ShowLogs", "Open logs window"), + ("ServerMoveUp", "Switch to previous server"), + ("ServerMoveDown", "Switch to next server"), + ]; + + private readonly WinUIPageContext _context; + private readonly CheckBox _registerAtStartup; + private readonly Dictionary _editors = new(StringComparer.Ordinal); + private readonly Dictionary _status = new(StringComparer.Ordinal); + private readonly Button _saveButton; + private readonly Button _discardButton; + private bool _refreshing; + + internal HotkeysPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Hotkeys", + "Configure global keyboard shortcuts.", + out StackPanel panel); + + var card = new StackPanel { Spacing = 12 }; + card.Children.Add(WinUIStyles.CreateSectionTitle("Global shortcuts")); + + var grid = new Grid { ColumnSpacing = 12, RowSpacing = 8 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(220) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) }); + + foreach ((string key, string label) in Definitions) + { + AddEditorRow(grid, key, label); + } + card.Children.Add(grid); + + _registerAtStartup = new CheckBox { Content = "Register hotkeys at startup" }; + _context.SetToolTip(_registerAtStartup, "Register the configured global hotkeys automatically when Shadowsocks starts."); + _registerAtStartup.Checked += (_, _) => MarkDirty(); + _registerAtStartup.Unchecked += (_, _) => MarkDirty(); + card.Children.Add(_registerAtStartup); + + var actions = new Grid { ColumnSpacing = 8 }; + actions.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + actions.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + actions.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + actions.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var registerAll = new Button { Content = "Register all", MinWidth = 110 }; + _context.SetToolTip(registerAll, "Try to register every configured global hotkey immediately."); + registerAll.Click += OnRegisterAllClicked; + actions.Children.Add(registerAll); + + _discardButton = new Button { Content = "Discard changes", MinWidth = 120, IsEnabled = false }; + _context.SetToolTip(_discardButton, "Discard unsaved hotkey edits and reload the saved shortcuts."); + _discardButton.Click += (_, _) => Refresh(); + Grid.SetColumn(_discardButton, 2); + actions.Children.Add(_discardButton); + + _saveButton = new Button { Content = "Save", MinWidth = 92, IsEnabled = false }; + _context.SetToolTip(_saveButton, "Save the hotkey configuration and apply it to the running application."); + _saveButton.Click += OnSaveClicked; + Grid.SetColumn(_saveButton, 3); + actions.Children.Add(_saveButton); + card.Children.Add(actions); + + panel.Children.Add(WinUIStyles.CreateCard(card)); + panel.Children.Add(new InfoBar + { + IsOpen = true, + IsClosable = false, + Severity = InfoBarSeverity.Informational, + Title = "Shortcut format", + Message = "Focus a shortcut field and press Ctrl/Alt/Shift/Win plus a key. Press Delete or Backspace with a modifier to use that key; use Esc to clear the field. The status column shows the last registration result.", + }); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + HotkeyConfig? config = _context.Controller?.GetCurrentConfiguration().hotkey; + if (config is null) + { + return; + } + + _refreshing = true; + try + { + _editors["SwitchSystemProxy"].Text = config.SwitchSystemProxy ?? string.Empty; + _editors["SwitchSystemProxyMode"].Text = config.SwitchSystemProxyMode ?? string.Empty; + _editors["SwitchAllowLan"].Text = config.SwitchAllowLan ?? string.Empty; + _editors["ShowLogs"].Text = config.ShowLogs ?? string.Empty; + _editors["ServerMoveUp"].Text = config.ServerMoveUp ?? string.Empty; + _editors["ServerMoveDown"].Text = config.ServerMoveDown ?? string.Empty; + _registerAtStartup.IsChecked = config.RegHotkeysAtStartup; + foreach (TextBlock status in _status.Values) + { + SetRegistrationStatus(status, "—", "Not tested"); + } + SetDirty(false); + } + finally + { + _refreshing = false; + } + } + + private void AddEditorRow(Grid grid, string key, string label) + { + int row = grid.RowDefinitions.Count; + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + TextBlock labelBlock = WinUIStyles.CreateText(_context.L(label)); + labelBlock.VerticalAlignment = VerticalAlignment.Center; + Grid.SetRow(labelBlock, row); + grid.Children.Add(labelBlock); + + var editor = new TextBox + { + PlaceholderText = "Ctrl+Alt+S", + HorizontalAlignment = HorizontalAlignment.Stretch, + IsReadOnly = true, + }; + _context.SetToolTip(editor, "Focus this field and press the desired modifier keys plus a key. Press Esc to clear it."); + editor.TextChanged += (_, _) => MarkDirty(); + editor.KeyDown += (_, args) => RecordKeyDown(key, args); + editor.KeyUp += (_, args) => FinishOnKeyUp(key, args); + _editors[key] = editor; + Grid.SetRow(editor, row); + Grid.SetColumn(editor, 1); + grid.Children.Add(editor); + + TextBlock status = WinUIStyles.CreateText("—", "BodyStrongTextBlockStyle"); + status.HorizontalAlignment = HorizontalAlignment.Center; + status.VerticalAlignment = VerticalAlignment.Center; + SetRegistrationStatus(status, "—", "Not tested"); + _status[key] = status; + Grid.SetRow(status, row); + Grid.SetColumn(status, 2); + grid.Children.Add(status); + } + + private void RecordKeyDown(string configKey, KeyRoutedEventArgs args) + { + args.Handled = true; + if (args.Key == VirtualKey.Escape) + { + _editors[configKey].Text = string.Empty; + return; + } + + var parts = new List(5); + if (IsKeyDown(VirtualKey.Control)) parts.Add("Ctrl"); + if (IsKeyDown(VirtualKey.Menu)) parts.Add("Alt"); + if (IsKeyDown(VirtualKey.Shift)) parts.Add("Shift"); + if (IsKeyDown(VirtualKey.LeftWindows) || IsKeyDown(VirtualKey.RightWindows)) parts.Add("Win"); + + if (parts.Count == 0) + { + _editors[configKey].Text = string.Empty; + return; + } + + if (!IsModifier(args.Key)) + { + string? keyName = ToHotkeyKeyName(args.Key); + if (!string.IsNullOrWhiteSpace(keyName)) + { + parts.Add(keyName); + } + } + + string value = string.Join("+", parts); + _editors[configKey].Text = IsModifier(args.Key) ? value + "+" : value; + } + + private void FinishOnKeyUp(string configKey, KeyRoutedEventArgs args) + { + args.Handled = true; + if (_editors[configKey].Text.EndsWith('+')) + { + _editors[configKey].Text = string.Empty; + } + } + + // Keep modifier-state lookup on Win32 so the WinUI frontend does not pull the older + // UWP XAML projection graph into this process. GetKeyState reports the state that + // belonged to the keyboard message currently being handled, which is exactly what the + // shortcut recorder needs. + private static bool IsKeyDown(VirtualKey key) + => GetKeyState((int)key) < 0; + + [LibraryImport("user32.dll")] + private static partial short GetKeyState(int virtualKey); + + private static bool IsModifier(VirtualKey key) + => key is VirtualKey.Control or VirtualKey.Menu or VirtualKey.Shift + or VirtualKey.LeftWindows or VirtualKey.RightWindows; + + private static string? ToHotkeyKeyName(VirtualKey key) + { + uint value = (uint)key; + if (value is >= 0x41 and <= 0x5A) + { + return ((char)value).ToString(); + } + if (value is >= 0x30 and <= 0x39) + { + return "D" + (char)value; + } + if (value is >= 0x60 and <= 0x69) + { + return "NumPad" + (value - 0x60).ToString(System.Globalization.CultureInfo.InvariantCulture); + } + if (value is >= 0x70 and <= 0x87) + { + return "F" + (value - 0x70 + 1).ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + return value switch + { + 0x08 => "Back", + 0x09 => "Tab", + 0x0D => "Enter", + 0x20 => "Space", + 0x21 => "PageUp", + 0x22 => "PageDown", + 0x23 => "End", + 0x24 => "Home", + 0x25 => "Left", + 0x26 => "Up", + 0x27 => "Right", + 0x28 => "Down", + 0x2D => "Insert", + 0x2E => "Delete", + 0xBA => "Oem1", + 0xBB => "OemPlus", + 0xBC => "OemComma", + 0xBD => "OemMinus", + 0xBE => "OemPeriod", + 0xBF => "Oem2", + 0xC0 => "Oem3", + 0xDB => "Oem4", + 0xDC => "Oem5", + 0xDD => "Oem6", + 0xDE => "Oem7", + _ => null, + }; + } + + private void OnRegisterAllClicked(object _, RoutedEventArgs _1) + { + HotkeyConfig? config = BuildConfig(); + if (config is null) + { + return; + } + + IReadOnlyList failures = _context.RegisterHotkeys(config); + UpdateRegistrationStatus(failures); + _context.ShowInfo( + "Hotkeys", + failures.Count == 0 ? _context.L("All configured shortcuts registered.") : _context.LF("Windows refused: {0}", string.Join(", ", failures)), + failures.Count == 0 ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + + private void OnSaveClicked(object _, RoutedEventArgs _1) + { + if (_refreshing) + { + return; + } + + HotkeyConfig? config = BuildConfig(); + if (config is null) + { + return; + } + + IReadOnlyList failures = _context.ApplyHotkeys(config); + UpdateRegistrationStatus(failures); + SetDirty(false); + _context.ShowInfo( + "Hotkeys", + failures.Count == 0 ? _context.L("Hotkeys saved and registered.") : _context.LF("Saved, but Windows refused: {0}", string.Join(", ", failures)), + failures.Count == 0 ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + + private HotkeyConfig? BuildConfig() + { + string[] invalid = _editors + .Where(pair => !string.IsNullOrWhiteSpace(pair.Value.Text) + && !HotkeyGesture.TryParse(pair.Value.Text.Trim(), out _)) + .Select(pair => _context.L(Definitions.First(definition => definition.Key == pair.Key).Label)) + .ToArray(); + if (invalid.Length > 0) + { + _context.ShowInfo("Hotkeys", _context.LF("Invalid shortcut: {0}", string.Join(", ", invalid)), InfoBarSeverity.Error); + return null; + } + + return new HotkeyConfig + { + SwitchSystemProxy = Normalize("SwitchSystemProxy"), + SwitchSystemProxyMode = Normalize("SwitchSystemProxyMode"), + SwitchAllowLan = Normalize("SwitchAllowLan"), + ShowLogs = Normalize("ShowLogs"), + ServerMoveUp = Normalize("ServerMoveUp"), + ServerMoveDown = Normalize("ServerMoveDown"), + RegHotkeysAtStartup = _registerAtStartup.IsChecked == true, + }; + } + + private void UpdateRegistrationStatus(IReadOnlyList failures) + { + foreach ((string key, TextBlock status) in _status) + { + bool failed = failures.Contains(key, StringComparer.Ordinal); + SetRegistrationStatus(status, failed ? "✖" : "✔", failed ? "Registration failed" : "Registered"); + } + } + + private void SetRegistrationStatus(TextBlock status, string glyph, string accessibilityKey) + { + status.Text = glyph; + string localizedStatus = _context.L(accessibilityKey); + ToolTipService.SetToolTip(status, localizedStatus); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(status, localizedStatus); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetHelpText(status, localizedStatus); + } + + private void MarkDirty() + { + if (!_refreshing) + { + SetDirty(true); + } + } + + private void SetDirty(bool dirty) + { + _saveButton.IsEnabled = dirty; + _discardButton.IsEnabled = dirty; + } + + private string Normalize(string key) + { + string value = _editors[key].Text?.Trim() ?? string.Empty; + return HotkeyGesture.TryParse(value, out HotkeyGesture gesture) + ? gesture.ToString() + : string.Empty; + } +} diff --git a/Shadowsocks.WinUI/Pages/LogsPage.cs b/Shadowsocks.WinUI/Pages/LogsPage.cs new file mode 100644 index 000000000..88d731a38 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/LogsPage.cs @@ -0,0 +1,943 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Documents; +using Microsoft.UI.Xaml.Media; +using Shadowsocks.Controller; +using Shadowsocks.Core.Logging; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class LogsPage : Page, IRefreshablePage, IDisposable +{ + private const int MaxTailBytes = 256 * 1024; + private const int MaxVisibleLogLines = 750; + private const double LogViewerHeight = 360; + private const int FontStyleBold = 1; + private const int FontStyleItalic = 2; + private const int TrafficWindowSeconds = 60; + private const string SparkLevels = "▁▂▃▄▅▆▇█"; + + private readonly WinUIPageContext _context; + private readonly TextBlock _logPath; + private readonly TextBlock _traffic; + private readonly TextBlock _chartScale; + private readonly TextBlock _chartCurrent; + private readonly TextBlock _inboundGraph; + private readonly TextBlock _outboundGraph; + private readonly StackPanel _toolbar; + private readonly RichTextBlock _viewer; + private readonly ScrollViewer _viewerScroll; + private readonly ToggleSwitch _autoRefresh; + private readonly ToggleSwitch _verboseToggle; + private readonly ToggleSwitch _pluginOutputToggle; + private readonly ToggleSwitch _dnsLogsToggle; + private readonly CheckBox _wrapText; + private readonly CheckBox _topMost; + private readonly DispatcherQueueTimer _timer; + private readonly DispatcherQueueTimer _viewerConfigSaveTimer; + private readonly Lock _loggingSettingsGate = new(); + + private long[] _inboundSamples = []; + private long[] _outboundSamples = []; + private bool _refreshing; + private bool _trafficSubscribed; + private long _lastRenderedLogLength = -1; + private DateTime _lastRenderedLogWriteUtc = DateTime.MinValue; + private string _lastViewerText = string.Empty; + private bool _lastViewerIsStandaloneMessage; + private Action? _cancelLogRefresh; + private bool _disposed; + private int _logRefreshGeneration; + private int _loggingPreferenceWrites; + private LogViewerConfig? _pendingViewerConfig; + + private enum LogLineSeverity + { + Normal, + Warning, + Error, + } + + internal LogsPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Logs", + "Live log viewer with traffic history, wrap, toolbar, and always-on-top settings.", + out StackPanel panel); + + var header = new StackPanel { Spacing = 10 }; + header.Children.Add(WinUIStyles.CreateSectionTitle("Log Viewer")); + _logPath = WinUIStyles.CreateText(LoggingConfigurator.LogFilePath, "CaptionTextBlockStyle"); + _logPath.Opacity = 0.72; + header.Children.Add(_logPath); + _traffic = WinUIStyles.CreateText("In: 0 B · Out: 0 B", "BodyStrongTextBlockStyle"); + header.Children.Add(_traffic); + + _toolbar = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + var clear = new Button { Content = "Clear logs" }; + _context.SetToolTip(clear, "Clear the current log file and the text shown in this viewer."); + clear.Click += (_, _) => ClearLogs(); + _toolbar.Children.Add(clear); + var refresh = new Button { Content = "Refresh" }; + _context.SetToolTip(refresh, "Reload the latest log contents immediately."); + refresh.Click += (_, _) => QueueLogRefresh(force: true); + _toolbar.Children.Add(refresh); + var openFile = new Button { Content = "Open file" }; + _context.SetToolTip(openFile, "Open the active log file with the default Windows application."); + openFile.Click += (_, _) => OpenPath(LoggingConfigurator.LogFilePath); + _toolbar.Children.Add(openFile); + var openFolder = new Button { Content = "Open location" }; + _context.SetToolTip(openFolder, "Open the folder that contains Shadowsocks log files."); + openFolder.Click += (_, _) => OpenPath(Path.GetDirectoryName(LoggingConfigurator.LogFilePath)); + _toolbar.Children.Add(openFolder); + header.Children.Add(_toolbar); + + var options = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 18 }; + _wrapText = new CheckBox { Content = "Wrap text" }; + _context.SetToolTip(_wrapText, "Wrap long log lines instead of requiring horizontal scrolling."); + _wrapText.Checked += OnViewerOptionChanged; + _wrapText.Unchecked += OnViewerOptionChanged; + options.Children.Add(_wrapText); + + _topMost = new CheckBox { Content = "Top most" }; + _context.SetToolTip(_topMost, "Keep the main Shadowsocks window above other windows while viewing logs."); + _topMost.Checked += OnViewerOptionChanged; + _topMost.Unchecked += OnViewerOptionChanged; + options.Children.Add(_topMost); + + _autoRefresh = new ToggleSwitch + { + Header = "Auto-refresh", + IsOn = true, + OnContent = "Every 2 seconds", + OffContent = "Paused", + }; + _context.SetToolTip(_autoRefresh, "Refresh the log viewer automatically every two seconds."); + _autoRefresh.Toggled += (_, _) => UpdateTimer(); + options.Children.Add(_autoRefresh); + header.Children.Add(options); + panel.Children.Add(WinUIStyles.CreateCard(header)); + + var loggingOptions = new StackPanel { Spacing = 12 }; + loggingOptions.Children.Add(WinUIStyles.CreateSectionTitle("Logging")); + var loggingOptionsRow = new Grid { ColumnSpacing = 24 }; + loggingOptionsRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + loggingOptionsRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + loggingOptionsRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + + _verboseToggle = new ToggleSwitch + { + Header = "Verbose Logging", + OnContent = "Debug", + OffContent = "Info", + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _context.SetToolTip(_verboseToggle, "Enable debug-level application logging. This can significantly increase log volume."); + _verboseToggle.Toggled += OnVerboseToggled; + loggingOptionsRow.Children.Add(_verboseToggle); + + _pluginOutputToggle = new ToggleSwitch + { + Header = "Show Plugin Output", + OnContent = "On", + OffContent = "Off", + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _context.SetToolTip(_pluginOutputToggle, "Include SIP003 plugin stdout and stderr events in the Shadowsocks log."); + _pluginOutputToggle.Toggled += OnPluginOutputToggled; + Grid.SetColumn(_pluginOutputToggle, 1); + loggingOptionsRow.Children.Add(_pluginOutputToggle); + + _dnsLogsToggle = new ToggleSwitch + { + Header = "Show DNS Logs", + OnContent = "On", + OffContent = "Off", + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _context.SetToolTip(_dnsLogsToggle, "Show DNSCrypt and DoH runtime diagnostics in the log viewer and allow DNS runtime output to be written. Resolver probe diagnostics and RTT details also require Verbose Logging."); + _dnsLogsToggle.Toggled += OnDnsLogsToggled; + Grid.SetColumn(_dnsLogsToggle, 2); + loggingOptionsRow.Children.Add(_dnsLogsToggle); + loggingOptions.Children.Add(loggingOptionsRow); + panel.Children.Add(WinUIStyles.CreateCard(loggingOptions)); + + var chart = new StackPanel { Spacing = 6 }; + chart.Children.Add(WinUIStyles.CreateSectionTitle("Traffic · last 60 seconds")); + _chartCurrent = WinUIStyles.CreateText("In 0 B/s · Out 0 B/s", "CaptionTextBlockStyle"); + chart.Children.Add(_chartCurrent); + _chartScale = WinUIStyles.CreateText("Scale 1 KiB/s", "CaptionTextBlockStyle"); + chart.Children.Add(_chartScale); + _inboundGraph = CreateTrafficGraphText(_context.L("Inbound") + " "); + _outboundGraph = CreateTrafficGraphText(_context.L("Outbound") + " "); + chart.Children.Add(_inboundGraph); + chart.Children.Add(_outboundGraph); + panel.Children.Add(WinUIStyles.CreateCard(chart)); + + _viewer = new RichTextBlock + { + IsTextSelectionEnabled = true, + TextWrapping = TextWrapping.NoWrap, + FontFamily = new FontFamily("Cascadia Mono, Consolas"), + FontSize = PointsToDip(8F), + Padding = new Thickness(10, 8, 10, 8), + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _viewerScroll = new ScrollViewer + { + Height = LogViewerHeight, + Content = _viewer, + HorizontalScrollMode = ScrollMode.Enabled, + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + VerticalScrollMode = ScrollMode.Enabled, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + }; + panel.Children.Add(WinUIStyles.CreateCard(_viewerScroll)); + + _timer = DispatcherQueue.CreateTimer(); + _timer.Interval = TimeSpan.FromSeconds(2); + _timer.IsRepeating = true; + _timer.Tick += (_, _) => QueueLogRefresh(); + + _viewerConfigSaveTimer = DispatcherQueue.CreateTimer(); + _viewerConfigSaveTimer.Interval = TimeSpan.FromMilliseconds(450); + _viewerConfigSaveTimer.IsRepeating = false; + _viewerConfigSaveTimer.Tick += (_, _) => FlushPendingViewerConfig(); + Loaded += OnLoaded; + Unloaded += OnUnloaded; + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + _refreshing = true; + try + { + _logPath.Text = LoggingConfigurator.LogFilePath; + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + LogViewerConfig config = _pendingViewerConfig ?? configuration?.logViewer ?? new LogViewerConfig(); + _wrapText.IsChecked = config.wrapText; + _topMost.IsChecked = config.topMost; + if (Volatile.Read(ref _loggingPreferenceWrites) == 0) + { + _verboseToggle.IsOn = configuration?.isVerboseLogging == true; + _pluginOutputToggle.IsOn = configuration?.showPluginOutput == true; + _dnsLogsToggle.IsOn = configuration?.showDnsLogs == true; + } + ApplyViewerConfig(config); + _context.SetAlwaysOnTop(config.topMost); + } + finally + { + _refreshing = false; + } + + RefreshTrafficSnapshot(); + QueueLogRefresh(); + UpdateTimer(); + } + + private async void OnVerboseToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + return; + + bool enabled = _verboseToggle.IsOn; + await PersistLoggingPreferenceAsync( + () => _context.Controller.ToggleVerboseLogging(enabled), + "Verbose Logging").ConfigureAwait(true); + } + + private async void OnPluginOutputToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + return; + + bool enabled = _pluginOutputToggle.IsOn; + await PersistLoggingPreferenceAsync( + () => _context.Controller.ToggleShowPluginOutput(enabled), + "Show Plugin Output").ConfigureAwait(true); + } + + private async void OnDnsLogsToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + return; + + bool enabled = _dnsLogsToggle.IsOn; + await PersistLoggingPreferenceAsync( + () => _context.Controller.ToggleShowDnsLogs(enabled), + "Show DNS Logs").ConfigureAwait(true); + } + + private async Task PersistLoggingPreferenceAsync(Action update, string settingName) + { + if (_disposed) + return; + + Interlocked.Increment(ref _loggingPreferenceWrites); + try + { + await Task.Run(() => + { + lock (_loggingSettingsGate) + { + update(); + } + }).ConfigureAwait(true); + RenderCurrentViewerText(); + QueueLogRefresh(force: true); + } + catch (Exception exception) + { + _context.ShowInfo("Logs", _context.LF("Unable to save {0}: {1}", settingName, exception.Message), InfoBarSeverity.Error); + SyncLoggingTogglesFromConfiguration(); + } + finally + { + Interlocked.Decrement(ref _loggingPreferenceWrites); + } + } + + private void SyncLoggingTogglesFromConfiguration() + { + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + if (configuration is null) + return; + + _refreshing = true; + try + { + _verboseToggle.IsOn = configuration.isVerboseLogging; + _pluginOutputToggle.IsOn = configuration.showPluginOutput; + _dnsLogsToggle.IsOn = configuration.showDnsLogs; + } + finally + { + _refreshing = false; + } + } + + private static TextBlock CreateTrafficGraphText(string label) + { + return new TextBlock + { + Text = label + new string('▁', TrafficWindowSeconds), + TextWrapping = TextWrapping.NoWrap, + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Cascadia Mono, Consolas"), + FontSize = 13, + }; + } + + private void OnLoaded(object _, RoutedEventArgs _1) + { + _context.SetAlwaysOnTop(_topMost.IsChecked == true); + SubscribeTraffic(); + RefreshTrafficSnapshot(); + QueueLogRefresh(); + UpdateTimer(); + } + + private void OnUnloaded(object _, RoutedEventArgs _1) + { + _timer.Stop(); + _viewerConfigSaveTimer.Stop(); + FlushPendingViewerConfig(); + CancelLogRefresh(); + UnsubscribeTraffic(); + _context.SetAlwaysOnTop(false); + } + + private void SubscribeTraffic() + { + if (_trafficSubscribed || _context.Controller is null) + { + return; + } + + _context.Controller.TrafficChanged += OnControllerTrafficChanged; + _trafficSubscribed = true; + } + + private void UnsubscribeTraffic() + { + if (!_trafficSubscribed || _context.Controller is null) + { + return; + } + + _context.Controller.TrafficChanged -= OnControllerTrafficChanged; + _trafficSubscribed = false; + } + + private void OnControllerTrafficChanged(object? _, EventArgs _1) + { + var controller = _context.Controller; + if (controller is null) + { + return; + } + + IReadOnlyList snapshot = controller.GetTrafficSnapshot(TrafficWindowSeconds); + long[] inbound = snapshot.Select(item => Math.Max(0, item.InboundIncrement)).ToArray(); + long[] outbound = snapshot.Select(item => Math.Max(0, item.OutboundIncrement)).ToArray(); + _ = DispatcherQueue.TryEnqueue(() => UpdateTrafficSamples(inbound, outbound)); + } + + private void RefreshTrafficSnapshot() + { + var controller = _context.Controller; + if (controller is null) + { + UpdateTrafficSamples([], []); + return; + } + + IReadOnlyList snapshot = controller.GetTrafficSnapshot(TrafficWindowSeconds); + UpdateTrafficSamples( + snapshot.Select(item => Math.Max(0, item.InboundIncrement)).ToArray(), + snapshot.Select(item => Math.Max(0, item.OutboundIncrement)).ToArray()); + } + + private void UpdateTrafficSamples(long[] inbound, long[] outbound) + { + _inboundSamples = inbound; + _outboundSamples = outbound; + UpdateTrafficText(); + RenderTrafficChart(); + } + + private void RenderTrafficChart() + { + int count = Math.Min(_inboundSamples.Length, _outboundSamples.Length); + if (count == 0) + { + _inboundGraph.Text = _context.L("Inbound") + " " + new string('▁', TrafficWindowSeconds); + _outboundGraph.Text = _context.L("Outbound") + " " + new string('▁', TrafficWindowSeconds); + _chartScale.Text = _context.L("Scale 1 KiB/s"); + _chartCurrent.Text = _context.L("In 0 B/s · Out 0 B/s"); + return; + } + + long maximum = 1024; + for (int index = 0; index < count; index++) + { + maximum = Math.Max(maximum, Math.Max(_inboundSamples[index], _outboundSamples[index])); + } + + _inboundGraph.Text = _context.L("Inbound") + " " + BuildSparkline(_inboundSamples, maximum); + _outboundGraph.Text = _context.L("Outbound") + " " + BuildSparkline(_outboundSamples, maximum); + _chartScale.Text = _context.LF("Scale {0}/s", FormatBytes(maximum)); + _chartCurrent.Text = _context.LF("In {0}/s · Out {1}/s", FormatBytes(_inboundSamples[count - 1]), FormatBytes(_outboundSamples[count - 1])); + } + + private static string BuildSparkline(long[] samples, long maximum) + { + var builder = new StringBuilder(TrafficWindowSeconds); + int padding = Math.Max(0, TrafficWindowSeconds - samples.Length); + builder.Append('▁', padding); + + foreach (long sample in samples.TakeLast(TrafficWindowSeconds)) + { + double normalized = maximum <= 0 ? 0 : Math.Clamp(sample / (double)maximum, 0, 1); + int level = (int)Math.Round(normalized * (SparkLevels.Length - 1)); + builder.Append(SparkLevels[level]); + } + + return builder.ToString(); + } + + private void OnViewerOptionChanged(object _, RoutedEventArgs _1) + { + ApplyWrapMode(); + _toolbar.Visibility = Visibility.Visible; + _context.SetAlwaysOnTop(_topMost.IsChecked == true); + + if (_refreshing || _context.Controller is null) + { + return; + } + + LogViewerConfig config = GetLogViewerConfig(); + config.wrapText = _wrapText.IsChecked == true; + config.topMost = _topMost.IsChecked == true; + config.toolbarShown = true; + _pendingViewerConfig = config; + _viewerConfigSaveTimer.Stop(); + _viewerConfigSaveTimer.Start(); + } + + private void ApplyViewerConfig(LogViewerConfig config) + { + string viewerFontFamily = string.IsNullOrWhiteSpace(config.fontFamily) ? "Consolas" : config.fontFamily; + double viewerFontSize = PointsToDip(config.fontSize > 0 ? config.fontSize : 8F); + int viewerFontStyle = config.fontStyle; + _viewer.FontFamily = new FontFamily(viewerFontFamily); + _viewer.FontSize = viewerFontSize; + var weight = _viewer.FontWeight; + weight.Weight = (ushort)(((viewerFontStyle & FontStyleBold) != 0) ? 700 : 400); + _viewer.FontWeight = weight; + Type fontStyleType = _viewer.FontStyle.GetType(); + _viewer.SetValue( + RichTextBlock.FontStyleProperty, + Enum.Parse(fontStyleType, (viewerFontStyle & FontStyleItalic) != 0 ? "Italic" : "Normal", ignoreCase: false)); + ApplyWrapMode(); + _toolbar.Visibility = Visibility.Visible; + } + + private LogViewerConfig GetLogViewerConfig() + => _context.Controller?.GetCurrentConfiguration().logViewer ?? new LogViewerConfig(); + + private void ApplyWrapMode() + { + bool viewerWrap = _wrapText.IsChecked == true; + _viewer.TextWrapping = viewerWrap ? TextWrapping.Wrap : TextWrapping.NoWrap; + _viewerScroll.HorizontalScrollMode = viewerWrap ? ScrollMode.Disabled : ScrollMode.Enabled; + _viewerScroll.HorizontalScrollBarVisibility = viewerWrap + ? ScrollBarVisibility.Disabled + : ScrollBarVisibility.Auto; + } + + private void FlushPendingViewerConfig() + { + LogViewerConfig? config = _pendingViewerConfig; + _pendingViewerConfig = null; + if (config is null || _context.Controller is null) + return; + + _ = PersistViewerConfigAsync(config); + } + + private async Task PersistViewerConfigAsync(LogViewerConfig config) + { + if (_disposed) + return; + + try + { + await Task.Run(() => + { + lock (_loggingSettingsGate) + { + _context.Controller?.SaveLogViewerConfig(config); + } + }).ConfigureAwait(true); + } + catch (Exception exception) + { + _context.ShowInfo("Logs", _context.LF("Unable to save log viewer settings: {0}", exception.Message), InfoBarSeverity.Error); + } + } + + private void UpdateTimer() + { + if (_autoRefresh.IsOn && IsLoaded) + { + if (!_timer.IsRunning) + { + _timer.Start(); + } + } + else + { + _timer.Stop(); + } + } + + private void QueueLogRefresh(bool force = false) + { + if (!IsLoaded && !force) + return; + + int generation = Interlocked.Increment(ref _logRefreshGeneration); + _cancelLogRefresh?.Invoke(); + _ = RefreshLogTextAsync(force, generation); + } + + private void CancelLogRefresh() + { + Interlocked.Increment(ref _logRefreshGeneration); + _cancelLogRefresh?.Invoke(); + _cancelLogRefresh = null; + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _timer.Stop(); + _viewerConfigSaveTimer.Stop(); + _pendingViewerConfig = null; + CancelLogRefresh(); + UnsubscribeTraffic(); + GC.SuppressFinalize(this); + } + + private async Task RefreshLogTextAsync(bool force, int generation) + { + using var cancellation = new CancellationTokenSource(); + Action cancel = cancellation.Cancel; + _cancelLogRefresh = cancel; + CancellationToken cancellationToken = cancellation.Token; + UpdateTrafficText(); + string path = LoggingConfigurator.LogFilePath; + _logPath.Text = path; + string truncatedMarker = _context.L("… showing the last 256 KiB …"); + long previousLength = _lastRenderedLogLength; + DateTime previousWriteUtc = _lastRenderedLogWriteUtc; + + try + { + LogTailSnapshot snapshot = await Task.Run( + () => ReadLogTail(path, previousLength, previousWriteUtc, truncatedMarker, force, cancellationToken), + cancellationToken).ConfigureAwait(true); + + if (cancellationToken.IsCancellationRequested || generation != _logRefreshGeneration) + return; + + if (!snapshot.Exists) + { + SetViewerMessage(_context.L("Log file has not been created yet.")); + _lastRenderedLogLength = -1; + _lastRenderedLogWriteUtc = DateTime.MinValue; + return; + } + + if (snapshot.Unchanged) + return; + + PopulateViewer(snapshot.Text); + _lastRenderedLogLength = snapshot.Length; + _lastRenderedLogWriteUtc = snapshot.LastWriteUtc; + } + catch (OperationCanceledException) + { + // A newer refresh superseded this one. + } + catch (Exception exception) + { + if (!cancellationToken.IsCancellationRequested && generation == _logRefreshGeneration) + SetViewerMessage(_context.LF("Unable to read log: {0}", exception.Message), LogLineSeverity.Error); + } + finally + { + if (ReferenceEquals(_cancelLogRefresh, cancel)) + _cancelLogRefresh = null; + } + } + + private static LogTailSnapshot ReadLogTail( + string path, + long previousLength, + DateTime previousWriteUtc, + string truncatedMarker, + bool force, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!File.Exists(path)) + return LogTailSnapshot.Missing; + + var fileInfo = new FileInfo(path); + if (!force && fileInfo.Length == previousLength && fileInfo.LastWriteTimeUtc == previousWriteUtc) + return new LogTailSnapshot(true, true, fileInfo.Length, fileInfo.LastWriteTimeUtc, string.Empty); + + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + long offset = Math.Max(0, stream.Length - MaxTailBytes); + stream.Seek(offset, SeekOrigin.Begin); + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + string text = reader.ReadToEnd(); + cancellationToken.ThrowIfCancellationRequested(); + + if (offset > 0) + { + int newline = text.IndexOf('\n'); + if (newline >= 0 && newline + 1 < text.Length) + text = text[(newline + 1)..]; + text = truncatedMarker + "\r\n" + text; + } + + text = TrimToLastLines(text, MaxVisibleLogLines); + return new LogTailSnapshot(true, false, fileInfo.Length, fileInfo.LastWriteTimeUtc, text); + } + + private static string TrimToLastLines(string text, int maxLines) + { + string normalized = (text ?? string.Empty) + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n'); + string[] lines = normalized.Split('\n'); + int firstLine = Math.Max(0, lines.Length - maxLines); + string visibleText = string.Join(Environment.NewLine, lines, firstLine, lines.Length - firstLine); + return visibleText.EndsWith(Environment.NewLine, StringComparison.Ordinal) + ? visibleText[..^Environment.NewLine.Length] + : visibleText; + } + + private void PopulateViewer(string text) + { + _lastViewerText = text; + _lastViewerIsStandaloneMessage = false; + SetViewerText(FilterVisibleLogLines(text)); + } + + private void SetViewerMessage(string message, LogLineSeverity severity = LogLineSeverity.Normal) + { + _lastViewerText = message; + _lastViewerIsStandaloneMessage = true; + SetViewerText(message); + } + + private void RenderCurrentViewerText() + { + SetViewerText(_lastViewerIsStandaloneMessage + ? _lastViewerText + : FilterVisibleLogLines(_lastViewerText)); + } + + private string FilterVisibleLogLines(string text) + { + if (string.IsNullOrEmpty(text)) + return string.Empty; + + bool showVerbose = _verboseToggle.IsOn; + bool showPluginOutput = _pluginOutputToggle.IsOn; + bool showDnsLogs = _dnsLogsToggle.IsOn; + string normalized = text + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n'); + string[] lines = normalized.Split('\n'); + var visible = new StringBuilder(normalized.Length); + bool includeContinuation = true; + + foreach (string line in lines) + { + string[] parts = line.Split('|', 4); + if (parts.Length >= 4) + { + string level = parts[1]?.Trim() ?? string.Empty; + string loggerName = parts[2]?.Trim() ?? string.Empty; + string message = parts[3] ?? string.Empty; + bool verboseLine = level.Equals("DEBUG", StringComparison.OrdinalIgnoreCase) + || level.Equals("TRACE", StringComparison.OrdinalIgnoreCase); + bool pluginLine = message.Contains("SIP003 |", StringComparison.OrdinalIgnoreCase) + || loggerName.EndsWith(".Sip003Plugin", StringComparison.OrdinalIgnoreCase); + bool dnsDiagnosticLine = message.Contains("DNSCryptProxy |", StringComparison.OrdinalIgnoreCase) + || loggerName.Contains("DnsCryptRuntimeManager", StringComparison.OrdinalIgnoreCase) + || loggerName.Contains("TransparentDohRelay", StringComparison.OrdinalIgnoreCase); + includeContinuation = (!verboseLine || showVerbose) + && (!pluginLine || showPluginOutput) + && (!dnsDiagnosticLine || showDnsLogs); + } + + if (!includeContinuation) + continue; + if (visible.Length > 0) + visible.AppendLine(); + visible.Append(line); + } + + return visible.ToString(); + } + + private void SetViewerText(string text) + { + bool wasAtBottom = _viewerScroll.ScrollableHeight <= 0 + || _viewerScroll.VerticalOffset >= _viewerScroll.ScrollableHeight - 24; + + _viewer.Blocks.Clear(); + var paragraph = new Paragraph(); + string normalized = (text ?? string.Empty) + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n'); + string[] lines = normalized.Split('\n'); + + Brush? secondary = TryGetThemeBrush("TextFillColorSecondaryBrush"); + Brush? warning = TryGetThemeBrush("SystemFillColorCautionBrush"); + Brush? error = TryGetThemeBrush("SystemFillColorCriticalBrush"); + LogLineSeverity inheritedSeverity = LogLineSeverity.Normal; + for (int index = 0; index < lines.Length; index++) + { + inheritedSeverity = AppendStyledLogLine(paragraph, lines[index], inheritedSeverity, secondary, warning, error); + if (index < lines.Length - 1) + paragraph.Inlines.Add(new LineBreak()); + } + _viewer.Blocks.Add(paragraph); + + if (wasAtBottom) + { + _ = DispatcherQueue.TryEnqueue(() => + _viewerScroll.ChangeView(null, _viewerScroll.ScrollableHeight, null, disableAnimation: true)); + } + } + + private static LogLineSeverity AppendStyledLogLine( + Paragraph paragraph, + string line, + LogLineSeverity inheritedSeverity, + Brush? secondary, + Brush? warning, + Brush? error) + { + if (string.IsNullOrEmpty(line)) + return inheritedSeverity; + + string[] parts = line.Split('|', 4); + if (parts.Length < 4) + { + Brush? continuationBrush = inheritedSeverity switch + { + LogLineSeverity.Error => error, + LogLineSeverity.Warning => warning, + _ => secondary, + }; + paragraph.Inlines.Add(CreateRun(" " + line, continuationBrush)); + return inheritedSeverity; + } + + LogLineSeverity severity = GetSeverity(parts[1]); + string level = parts[1]?.Trim() ?? string.Empty; + Brush? lineBrush = severity switch + { + LogLineSeverity.Error => error, + LogLineSeverity.Warning => warning, + _ when level.Equals("DEBUG", StringComparison.OrdinalIgnoreCase) + || level.Equals("TRACE", StringComparison.OrdinalIgnoreCase) => secondary, + _ => null, + }; + paragraph.Inlines.Add(CreateRun(line, lineBrush, bold: severity != LogLineSeverity.Normal)); + return severity; + } + + private static Run CreateRun(string text, Brush? foreground, bool bold = false) + { + var run = new Run { Text = text }; + if (foreground is not null) + run.Foreground = foreground; + if (bold) + { + var weight = run.FontWeight; + weight.Weight = 700; + run.FontWeight = weight; + } + return run; + } + + private static LogLineSeverity GetSeverity(string level) + { + string value = level?.Trim() ?? string.Empty; + if (value.Equals("ERROR", StringComparison.OrdinalIgnoreCase) + || value.Equals("FATAL", StringComparison.OrdinalIgnoreCase) + || value.Equals("CRITICAL", StringComparison.OrdinalIgnoreCase)) + { + return LogLineSeverity.Error; + } + if (value.Equals("WARN", StringComparison.OrdinalIgnoreCase) + || value.Equals("WARNING", StringComparison.OrdinalIgnoreCase)) + { + return LogLineSeverity.Warning; + } + return LogLineSeverity.Normal; + } + + private static Brush? TryGetThemeBrush(string key) + { + ResourceDictionary? resources = Application.Current?.Resources; + if (resources is null) + return null; + try + { + return resources[key] as Brush; + } + catch (Exception) + { + return null; + } + } + + private void ClearLogs() + { + try + { + string path = LoggingConfigurator.LogFilePath; + if (File.Exists(path)) + { + File.Delete(path); + } + _viewer.Blocks.Clear(); + _lastViewerText = string.Empty; + _lastViewerIsStandaloneMessage = false; + _lastRenderedLogLength = -1; + _lastRenderedLogWriteUtc = DateTime.MinValue; + _context.ShowInfo("Logs", "Log file cleared.", InfoBarSeverity.Success); + } + catch (Exception exception) + { + _context.ShowInfo("Logs", exception.Message, InfoBarSeverity.Error); + } + } + + private void UpdateTrafficText() + { + if (_context.Controller is null) + { + _traffic.Text = _context.L("Traffic unavailable"); + return; + } + _traffic.Text = _context.LF("In: {0} · Out: {1}", FormatBytes(_context.Controller.InboundCounter), FormatBytes(_context.Controller.OutboundCounter)); + } + + private static double PointsToDip(float points) => points * 96.0 / 72.0; + + private static string FormatBytes(long bytes) + { + string[] units = ["B", "KiB", "MiB", "GiB", "TiB"]; + double value = Math.Max(0, bytes); + int unit = 0; + while (value >= 1024 && unit < units.Length - 1) + { + value /= 1024; + unit++; + } + return unit == 0 ? $"{value:0} {units[unit]}" : $"{value:0.##} {units[unit]}"; + } + + private void OpenPath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + } + catch (Exception exception) + { + _context.ShowInfo("Logs", exception.Message, InfoBarSeverity.Error); + } + } + private sealed record LogTailSnapshot(bool Exists, bool Unchanged, long Length, DateTime LastWriteUtc, string Text) + { + public static LogTailSnapshot Missing { get; } = new(false, false, -1, DateTime.MinValue, string.Empty); + } + +} diff --git a/Shadowsocks.WinUI/Pages/OnlineConfigPage.cs b/Shadowsocks.WinUI/Pages/OnlineConfigPage.cs new file mode 100644 index 000000000..ef185f339 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/OnlineConfigPage.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; +using Windows.ApplicationModel.DataTransfer; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class OnlineConfigPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly ListView _sources; + private readonly TextBox _url; + private readonly Button _update; + private readonly Button _updateAll; + private readonly Button _copy; + private readonly Button _remove; + private readonly Button _add; + private readonly ProgressRing _progress; + private readonly List _items = new(); + private bool _busy; + + internal OnlineConfigPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Online Config", + "Manage SIP008 / online configuration sources.", + out StackPanel panel); + + var card = new Grid { ColumnSpacing = 12, RowSpacing = 12 }; + card.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + card.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + card.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + card.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + _sources = new ListView + { + SelectionMode = ListViewSelectionMode.Single, + MinHeight = 300, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _sources.SelectionChanged += (_, _) => UpdateButtons(); + _context.SetToolTip(_sources, "Select an online configuration source to update, copy, or remove it."); + card.Children.Add(_sources); + + var sourceActions = new StackPanel { Spacing = 8, MinWidth = 120 }; + _update = new Button { Content = "Update", HorizontalAlignment = HorizontalAlignment.Stretch }; + _context.SetToolTip(_update, "Download and apply the selected online configuration source now."); + _update.Click += OnUpdateClicked; + sourceActions.Children.Add(_update); + _updateAll = new Button { Content = "Update all", HorizontalAlignment = HorizontalAlignment.Stretch }; + _context.SetToolTip(_updateAll, "Download and apply every configured online source."); + _updateAll.Click += OnUpdateAllClicked; + sourceActions.Children.Add(_updateAll); + _copy = new Button { Content = "Copy link", HorizontalAlignment = HorizontalAlignment.Stretch }; + _context.SetToolTip(_copy, "Copy the selected online configuration URL to the clipboard."); + _copy.Click += OnCopyClicked; + sourceActions.Children.Add(_copy); + _remove = new Button { Content = "Remove", HorizontalAlignment = HorizontalAlignment.Stretch }; + _context.SetToolTip(_remove, "Remove the selected online configuration source."); + _remove.Click += OnRemoveClicked; + sourceActions.Children.Add(_remove); + _progress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + HorizontalAlignment = HorizontalAlignment.Center, + }; + sourceActions.Children.Add(_progress); + Grid.SetColumn(sourceActions, 1); + card.Children.Add(sourceActions); + + var addGrid = new Grid { ColumnSpacing = 8 }; + addGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + addGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + _url = new TextBox { PlaceholderText = "https://example.com/subscription.json" }; + _context.SetToolTip(_url, "Enter an absolute HTTP or HTTPS SIP008 / online configuration URL."); + _url.TextChanged += (_, _) => UpdateButtons(); + addGrid.Children.Add(_url); + _add = new Button { Content = "Add", MinWidth = 96 }; + _context.SetToolTip(_add, "Add the entered URL to the online configuration source list."); + _add.Click += OnAddClicked; + Grid.SetColumn(_add, 1); + addGrid.Children.Add(_add); + Grid.SetRow(addGrid, 1); + Grid.SetColumnSpan(addGrid, 2); + card.Children.Add(addGrid); + + panel.Children.Add(WinUIStyles.CreateCard(card)); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Configuration? config = _context.Controller?.GetCurrentConfiguration(); + if (config is null) + { + return; + } + + string? previous = _sources.SelectedItem?.ToString(); + _items.Clear(); + _items.AddRange(config.onlineConfigSource ?? []); + Rebuild(previous); + } + + private void Rebuild(string? select = null) + { + _sources.Items.Clear(); + foreach (string item in _items) + { + _sources.Items.Add(item); + } + if (!string.IsNullOrWhiteSpace(select)) + { + _sources.SelectedItem = _sources.Items.Cast() + .FirstOrDefault(item => string.Equals(item?.ToString(), select, StringComparison.Ordinal)); + } + UpdateButtons(); + } + + private void UpdateButtons() + { + bool idle = !_busy; + bool hasSelection = _sources.SelectedIndex >= 0; + _sources.IsEnabled = idle; + _url.IsEnabled = idle; + _update.IsEnabled = idle && hasSelection; + _copy.IsEnabled = idle && hasSelection; + _remove.IsEnabled = idle && hasSelection; + _updateAll.IsEnabled = idle && _items.Count > 0; + string candidate = _url.Text?.Trim() ?? string.Empty; + _add.IsEnabled = idle + && IsValidUrl(candidate) + && !_items.Contains(candidate, StringComparer.OrdinalIgnoreCase); + } + + private void SetBusy(bool busy) + { + _busy = busy; + _progress.IsActive = busy; + _progress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + UpdateButtons(); + } + + private async void OnUpdateClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || _sources.SelectedItem is not string url) + { + return; + } + + SetBusy(true); + try + { + bool success = await _context.Controller.UpdateOnlineConfig(url); + _context.ShowInfo("Online Config", success ? "Online configuration updated." : "Online configuration update failed.", + success ? InfoBarSeverity.Success : InfoBarSeverity.Error); + } + catch (Exception exception) + { + _context.ShowInfo("Online Config", exception.Message, InfoBarSeverity.Error); + } + finally + { + SetBusy(false); + } + } + + private async void OnUpdateAllClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + SetBusy(true); + try + { + List failed = await _context.Controller.UpdateAllOnlineConfig(); + _context.ShowInfo( + "Online Config", + failed.Count == 0 ? _context.L("All online configurations updated.") : _context.LF("Failed sources: {0}", string.Join(", ", failed)), + failed.Count == 0 ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + catch (Exception exception) + { + _context.ShowInfo("Online Config", exception.Message, InfoBarSeverity.Error); + } + finally + { + SetBusy(false); + } + } + + private void OnCopyClicked(object _, RoutedEventArgs _1) + { + if (_sources.SelectedItem is not string url) + { + return; + } + + var package = new DataPackage(); + package.SetText(url); + Clipboard.SetContent(package); + Clipboard.Flush(); + _context.ShowInfo("Online Config", "Source URL copied.", InfoBarSeverity.Success); + } + + private void OnRemoveClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || _sources.SelectedItem is not string url) + { + return; + } + + _items.RemoveAll(item => string.Equals(item, url, StringComparison.Ordinal)); + _context.Controller.RemoveOnlineConfig(url); + Rebuild(); + } + + private void OnAddClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + string url = _url.Text?.Trim() ?? string.Empty; + if (!IsValidUrl(url)) + { + _context.ShowInfo("Online Config", "Enter an absolute HTTP or HTTPS URL.", InfoBarSeverity.Error); + return; + } + + if (!_items.Contains(url, StringComparer.OrdinalIgnoreCase)) + { + _items.Add(url); + _context.Controller.SaveOnlineConfigSource(_items.ToList()); + } + _url.Text = string.Empty; + Rebuild(url); + } + + private static bool IsValidUrl(string? value) + => Uri.TryCreate(value?.Trim(), UriKind.Absolute, out Uri? uri) + && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp); +} diff --git a/Shadowsocks.WinUI/Pages/OverviewPage.cs b/Shadowsocks.WinUI/Pages/OverviewPage.cs new file mode 100644 index 000000000..73dc1d596 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/OverviewPage.cs @@ -0,0 +1,84 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class OverviewPage : Page, IRefreshablePage +{ + private readonly TextBlock _serverValue; + private readonly TextBlock _statusValue; + private readonly TextBlock _modeValue; + private readonly TextBlock _proxyValue; + private readonly TeachingTip _trayTip; + private readonly WinUIPageContext _context; + + internal OverviewPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Overview", + "Current proxy, server, and system-shell state at a glance.", + out StackPanel panel); + + var stateStack = new StackPanel { Spacing = 12 }; + stateStack.Children.Add(WinUIStyles.CreateSectionTitle("Connection state")); + stateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Server", out _serverValue)); + stateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Controller", out _statusValue)); + stateStack.Children.Add(WinUIStyles.CreateKeyValueRow("Traffic Mode", out _modeValue)); + stateStack.Children.Add(WinUIStyles.CreateKeyValueRow("System Proxy", out _proxyValue)); + panel.Children.Add(WinUIStyles.CreateCard(stateStack)); + + var backgroundStack = new StackPanel { Spacing = 12 }; + backgroundStack.Children.Add(WinUIStyles.CreateSectionTitle("Runs quietly in the background")); + var explanation = WinUIStyles.CreateText( + "Closing the window hides it to the notification area. The proxy controller keeps running until you choose Exit from the tray menu."); + explanation.Opacity = 0.8; + backgroundStack.Children.Add(explanation); + + var tipButton = new Button { Content = "How tray mode works", HorizontalAlignment = HorizontalAlignment.Left }; + _context.SetToolTip(tipButton, "Explain how closing or hiding the main window differs from exiting the tray application."); + backgroundStack.Children.Add(tipButton); + panel.Children.Add(WinUIStyles.CreateCard(backgroundStack)); + + _trayTip = new TeachingTip + { + Title = "Background mode", + Subtitle = "Use the tray icon to reopen Shadowsocks Reborn. A plain second launch points you back to the tray instead of starting another proxy.", + Target = tipButton, + PreferredPlacement = TeachingTipPlacementMode.Bottom, + IsLightDismissEnabled = true, + }; + panel.Children.Add(_trayTip); + tipButton.Click += (_, _) => _trayTip.IsOpen = true; + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + ShadowsocksController? controller = _context.Controller; + if (controller is null) + { + _serverValue.Text = _context.L("Unavailable"); + _statusValue.Text = _context.L("Failed to start"); + _modeValue.Text = "—"; + _proxyValue.Text = "—"; + return; + } + + Configuration configuration = controller.GetCurrentConfiguration(); + Server? server = controller.GetCurrentServer(); + _serverValue.Text = server?.IsConfigured == true ? server.ToString() : _context.L("Not configured"); + _statusValue.Text = controller.IsProxyListenerRunning ? _context.L("Running") : _context.L("Stopped"); + _modeValue.Text = _context.L(controller.GetTrafficRuntimeMode().ToString()); + _proxyValue.Text = configuration.Enabled + ? configuration.global ? _context.L("Enabled · Global") : _context.L("Enabled · PAC") + : _context.L("Disabled"); + } +} diff --git a/Shadowsocks.WinUI/Pages/PacGeositePage.cs b/Shadowsocks.WinUI/Pages/PacGeositePage.cs new file mode 100644 index 000000000..4dc1b91b3 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/PacGeositePage.cs @@ -0,0 +1,692 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Windows.ApplicationModel.DataTransfer; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class PacGeositePage : Page, IRefreshablePage +{ + private static readonly char[] SourceSeparators = ['\r', '\n', ';']; + private readonly WinUIPageContext _context; + private readonly ToggleSwitch _onlinePacToggle; + private readonly TextBox _pacUrl; + private readonly ToggleSwitch _securePacToggle; + private readonly ToggleSwitch _regeneratePacToggle; + private readonly Button _savePacUrlButton; + private readonly StackPanel _onlinePacOptions; + private readonly StackPanel _localPacOptions; + private readonly Button _copyLocalPacUrlButton; + private readonly TextBlock _effectiveRoutingMode; + private readonly TextBlock _snapshotStatus; + private readonly TextBlock _ruleStatus; + private readonly TextBlock _decisionStatus; + private readonly TextBlock _adminRoutingStatus; + private readonly ListView _sources; + private readonly Border _routingCard; + private readonly Border _userRulesCard; + private readonly Border _sourceCard; + private readonly TextBox _newSources; + private readonly Button _moveSourceUpButton; + private readonly Button _moveSourceDownButton; + private readonly Button _removeSourceButton; + private readonly Button _addSourceButton; + private readonly Button _saveSourcesButton; + private readonly Button _refreshGeoSiteButton; + private readonly Button _restoreDefaultButton; + private readonly ProgressRing _geoSiteProgress; + private readonly TextBlock _sourcePendingNote; + private readonly List _sourceItems = new(); + private bool _refreshing; + private bool _sourcesDirty; + private bool _geoSiteRefreshRunning; + private string _savedPacUrl = string.Empty; + + internal PacGeositePage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "PAC / GeoSite", + "Choose Local GeoSite PAC or an Online PAC and manage the ordered GeoSite source list.", + out StackPanel panel); + + var pacCard = new StackPanel { Spacing = 12 }; + pacCard.Children.Add(WinUIStyles.CreateSectionTitle("PAC mode")); + _onlinePacToggle = new ToggleSwitch + { + Header = "Online PAC", + OnContent = "Online", + OffContent = "Local GeoSite", + }; + _onlinePacToggle.Toggled += OnOnlinePacToggled; + _context.SetToolTip(_onlinePacToggle, "Switch between an online PAC URL and the locally generated GeoSite PAC."); + pacCard.Children.Add(_onlinePacToggle); + + _onlinePacOptions = new StackPanel { Spacing = 10 }; + _pacUrl = new TextBox + { + Header = "Online PAC URL", + PlaceholderText = "https://example.invalid/proxy.pac", + }; + _context.SetToolTip(_pacUrl, "URL downloaded and served when Online PAC mode is enabled."); + _pacUrl.TextChanged += (_, _) => UpdatePacActionState(); + _onlinePacOptions.Children.Add(_pacUrl); + _savePacUrlButton = new Button { Content = "Save PAC URL", HorizontalAlignment = HorizontalAlignment.Left }; + _savePacUrlButton.Click += OnSavePacUrlClicked; + _context.SetToolTip(_savePacUrlButton, "Save the online PAC URL used when Online PAC mode is enabled."); + _onlinePacOptions.Children.Add(_savePacUrlButton); + pacCard.Children.Add(_onlinePacOptions); + + _localPacOptions = new StackPanel { Spacing = 10 }; + _securePacToggle = new ToggleSwitch + { + Header = "Require secret for local PAC URL", + OnContent = "Secure", + OffContent = "Open", + }; + _securePacToggle.Toggled += OnSecurePacToggled; + _context.SetToolTip(_securePacToggle, "Require a random secret in the local PAC URL before the PAC file is served."); + _localPacOptions.Children.Add(_securePacToggle); + + _regeneratePacToggle = new ToggleSwitch + { + Header = "Regenerate Local PAC after application updates", + OnContent = "On", + OffContent = "Off", + }; + _regeneratePacToggle.Toggled += OnRegeneratePacToggled; + _context.SetToolTip(_regeneratePacToggle, "Rebuild the local PAC automatically after application or GeoSite data updates."); + _localPacOptions.Children.Add(_regeneratePacToggle); + + _copyLocalPacUrlButton = new Button { Content = "Copy Local PAC URL", HorizontalAlignment = HorizontalAlignment.Left }; + _copyLocalPacUrlButton.Click += OnCopyLocalPacUrlClicked; + _context.SetToolTip(_copyLocalPacUrlButton, "Copy the local PAC endpoint URL to the clipboard."); + _localPacOptions.Children.Add(_copyLocalPacUrlButton); + pacCard.Children.Add(_localPacOptions); + panel.Children.Add(WinUIStyles.CreateCard(pacCard)); + + var routingCard = new StackPanel { Spacing = 10 }; + routingCard.Children.Add(WinUIStyles.CreateSectionTitle("Local managed routing")); + routingCard.Children.Add(WinUIStyles.CreateText( + "Local GeoSite and ABP/EasyList network rules are always evaluated by the managed C# FilterEngine. The local PAC contains only a minimal proxy funnel and never evaluates filtering rules.")); + + _effectiveRoutingMode = WinUIStyles.CreateText(string.Empty); + _snapshotStatus = WinUIStyles.CreateText(string.Empty, "CaptionTextBlockStyle"); + _ruleStatus = WinUIStyles.CreateText(string.Empty, "CaptionTextBlockStyle"); + _decisionStatus = WinUIStyles.CreateText(string.Empty, "CaptionTextBlockStyle"); + _adminRoutingStatus = WinUIStyles.CreateText(string.Empty, "CaptionTextBlockStyle"); + routingCard.Children.Add(_effectiveRoutingMode); + routingCard.Children.Add(_snapshotStatus); + routingCard.Children.Add(_ruleStatus); + routingCard.Children.Add(_decisionStatus); + routingCard.Children.Add(_adminRoutingStatus); + + var refreshRoutingStatus = new Button + { + Content = "Refresh routing diagnostics", + HorizontalAlignment = HorizontalAlignment.Left, + }; + refreshRoutingStatus.Click += (_, _) => Refresh(); + _context.SetToolTip(refreshRoutingStatus, "Refresh the local managed-routing generation, rule counts and decision counters shown above."); + routingCard.Children.Add(refreshRoutingStatus); + _routingCard = WinUIStyles.CreateCard(routingCard); + panel.Children.Add(_routingCard); + + var userRulesStack = new StackPanel { Spacing = 10 }; + userRulesStack.Children.Add(WinUIStyles.CreateSectionTitle("Local user rules")); + userRulesStack.Children.Add(WinUIStyles.CreateText( + "Edit user-rule.txt to add your own ABP/EasyList-style network routing rules. User rules are evaluated before GeoSite defaults; normal rules use the proxy and @@ exception rules go direct. Changes are watched and applied automatically.")); + var openUserRules = new Button + { + Content = "Open user-rule.txt", + HorizontalAlignment = HorizontalAlignment.Left, + }; + openUserRules.Click += OnOpenUserRulesClicked; + _context.SetToolTip(openUserRules, "Create or open the active user-rule.txt file in your default text editor."); + userRulesStack.Children.Add(openUserRules); + _userRulesCard = WinUIStyles.CreateCard(userRulesStack); + panel.Children.Add(_userRulesCard); + + var sourceCard = new StackPanel { Spacing = 12 }; + sourceCard.Children.Add(WinUIStyles.CreateSectionTitle("GeoSite Sources")); + sourceCard.Children.Add(WinUIStyles.CreateText( + "Sources are tried independently and merged. A missing .sha256sum sidecar is non-fatal.")); + + _sources = new ListView + { + SelectionMode = ListViewSelectionMode.Single, + MinHeight = 150, + MaxHeight = 260, + }; + _sources.SelectionChanged += (_, _) => UpdateSourceActionState(); + _context.SetToolTip(_sources, "Select a GeoSite source before moving or removing it."); + sourceCard.Children.Add(_sources); + + var reorderActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _moveSourceUpButton = new Button { Content = "Move up" }; + _context.SetToolTip(_moveSourceUpButton, "Move the selected GeoSite source one position up."); + _moveSourceUpButton.Click += (_, _) => MoveSelected(-1); + reorderActions.Children.Add(_moveSourceUpButton); + _moveSourceDownButton = new Button { Content = "Move down" }; + _context.SetToolTip(_moveSourceDownButton, "Move the selected GeoSite source one position down."); + _moveSourceDownButton.Click += (_, _) => MoveSelected(1); + reorderActions.Children.Add(_moveSourceDownButton); + _removeSourceButton = new Button { Content = "Remove" }; + _context.SetToolTip(_removeSourceButton, "Remove the selected GeoSite source from the list."); + _removeSourceButton.Click += OnRemoveSelected; + reorderActions.Children.Add(_removeSourceButton); + _restoreDefaultButton = new Button { Content = "Restore default" }; + _context.SetToolTip(_restoreDefaultButton, "Restore the built-in default GeoSite source list."); + _restoreDefaultButton.Click += OnRestoreDefault; + reorderActions.Children.Add(_restoreDefaultButton); + sourceCard.Children.Add(reorderActions); + + _newSources = new TextBox + { + Header = "Add source URL(s)", + PlaceholderText = "One URL per line", + AcceptsReturn = true, + MinHeight = 72, + TextWrapping = TextWrapping.Wrap, + }; + _context.SetToolTip(_newSources, "Enter one or more GeoSite source URLs, one per line."); + _newSources.TextChanged += (_, _) => UpdateSourceActionState(); + sourceCard.Children.Add(_newSources); + + var sourceActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _addSourceButton = new Button { Content = "Add" }; + _context.SetToolTip(_addSourceButton, "Append the entered URLs to the GeoSite source list."); + _addSourceButton.Click += OnAddSources; + sourceActions.Children.Add(_addSourceButton); + _saveSourcesButton = new Button { Content = "Save sources", IsEnabled = false }; + _context.SetToolTip(_saveSourcesButton, "Save the current GeoSite source order and URLs."); + _saveSourcesButton.Click += OnSaveSources; + sourceActions.Children.Add(_saveSourcesButton); + _refreshGeoSiteButton = new Button { Content = "Download / refresh GeoSite" }; + _context.SetToolTip(_refreshGeoSiteButton, "Download the configured GeoSite data now and rebuild dependent local PAC data."); + _refreshGeoSiteButton.Click += OnRefreshClicked; + sourceActions.Children.Add(_refreshGeoSiteButton); + _geoSiteProgress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + VerticalAlignment = VerticalAlignment.Center, + }; + sourceActions.Children.Add(_geoSiteProgress); + sourceCard.Children.Add(sourceActions); + _sourcePendingNote = WinUIStyles.CreateText( + "Save GeoSite source changes before downloading or refreshing data.", + "CaptionTextBlockStyle"); + _sourcePendingNote.Opacity = 0.72; + _sourcePendingNote.Visibility = Visibility.Collapsed; + sourceCard.Children.Add(_sourcePendingNote); + _sourceCard = WinUIStyles.CreateCard(sourceCard); + panel.Children.Add(_sourceCard); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + if (configuration is null) + { + return; + } + + _refreshing = true; + try + { + _onlinePacToggle.IsOn = configuration.useOnlinePac; + _savedPacUrl = configuration.pacUrl?.Trim() ?? string.Empty; + _pacUrl.Text = _savedPacUrl; + _securePacToggle.IsOn = configuration.secureLocalPac; + _securePacToggle.IsEnabled = true; + _regeneratePacToggle.IsOn = configuration.regeneratePacOnUpdate; + ApplyPacModeState(configuration.useOnlinePac); + if (!configuration.useOnlinePac) + { + RefreshRoutingDiagnostics(); + } + + _sourceItems.Clear(); + _sourceItems.AddRange(Configuration.NormalizeGeositeSourceList(configuration.geositeUrls)); + _sourcesDirty = false; + RebuildSourceList(); + } + finally + { + _refreshing = false; + } + } + + private void RefreshRoutingDiagnostics() + { + if (_context.Controller is null) + { + return; + } + + ManagedRoutingStatus status = _context.Controller.GetManagedRoutingStatus(); + _effectiveRoutingMode.Text = _context.LF("Effective routing mode: {0}", _context.L(status.EffectiveMode)); + _snapshotStatus.Text = _context.LF( + "Snapshot: {0}, generation {1}, source: {2}", + _context.L(status.SnapshotMode), + status.Generation, + _context.L(status.Source)); + _ruleStatus.Text = _context.LF( + "Rules: {0} default + {1} user; invalid: {2}", + status.DefaultRuleCount, + status.UserRuleCount, + status.InvalidRuleCount); + _decisionStatus.Text = _context.LF( + "Managed decisions in this snapshot: {0} direct / {1} proxy", + status.DirectDecisionCount, + status.ProxyDecisionCount); + _adminRoutingStatus.Text = status.AdminManagedRoutingActive + ? _context.LF("Administrator relay: managed routing active ({0} rules)", status.AdminManagedRoutingRuleCount) + : _context.L("Administrator relay: managed routing inactive"); + } + + private void OnOpenUserRulesClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || !IsLocalPacMode()) + { + return; + } + + _context.Controller.TouchUserRuleFile(); + } + + private void OnCopyLocalPacUrlClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + var package = new DataPackage(); + package.SetText(_context.Controller.GetPacUrl()); + Clipboard.SetContent(package); + Clipboard.Flush(); + _context.ShowInfo("PAC", "Local PAC URL copied", InfoBarSeverity.Success); + } + + private void RebuildSourceList(int selectedIndex = -1) + { + _sources.Items.Clear(); + foreach (string source in _sourceItems) + { + _sources.Items.Add(source); + } + + if (_sourceItems.Count > 0) + { + _sources.SelectedIndex = selectedIndex >= 0 + ? Math.Min(selectedIndex, _sourceItems.Count - 1) + : Math.Min(_sources.SelectedIndex, _sourceItems.Count - 1); + } + else + { + _sources.SelectedIndex = -1; + } + UpdateSourceActionState(); + } + + private async void OnOnlinePacToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + if (!_onlinePacToggle.IsOn) + { + _context.Controller.UseOnlinePAC(false); + ApplyPacModeState(useOnlinePac: false); + return; + } + + Configuration configuration = _context.Controller.GetCurrentConfiguration(); + string pacUrl = configuration.pacUrl?.Trim() ?? string.Empty; + + if (!IsValidPacUrl(pacUrl)) + { + _onlinePacToggle.IsEnabled = false; + try + { + string? enteredUrl = await PromptForOnlinePacUrlAsync(); + if (string.IsNullOrWhiteSpace(enteredUrl)) + { + SetOnlinePacToggleWithoutEvent(false); + ApplyPacModeState(useOnlinePac: false); + return; + } + + pacUrl = enteredUrl; + _context.Controller.SavePACUrl(pacUrl); + _savedPacUrl = pacUrl; + _pacUrl.Text = pacUrl; + } + finally + { + _onlinePacToggle.IsEnabled = true; + } + } + + _context.Controller.UseOnlinePAC(true); + ApplyPacModeState(useOnlinePac: true); + } + + private void OnSavePacUrlClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + string url = _pacUrl.Text?.Trim() ?? string.Empty; + if (!IsValidPacUrl(url)) + { + _context.ShowInfo("PAC", "Enter an absolute HTTP or HTTPS PAC URL.", InfoBarSeverity.Error); + return; + } + + _context.Controller.SavePACUrl(url); + _savedPacUrl = url; + UpdatePacActionState(); + _context.ShowInfo("PAC", "Online PAC URL saved.", InfoBarSeverity.Success); + } + + private void OnSecurePacToggled(object _, RoutedEventArgs _1) + { + if (!_refreshing && _context.Controller is not null) + { + _context.Controller.ToggleSecureLocalPac(_securePacToggle.IsOn); + } + } + + private void OnRegeneratePacToggled(object _, RoutedEventArgs _1) + { + if (!_refreshing && _context.Controller is not null) + { + _context.Controller.ToggleRegeneratePacOnUpdate(_regeneratePacToggle.IsOn); + } + } + + private void OnAddSources(object _, RoutedEventArgs _1) + { + if (!IsLocalPacMode()) + { + return; + } + + List parsed = ParseSources(_newSources.Text); + if (parsed.Count == 0 || parsed.Any(source => !IsValidSource(source))) + { + _context.ShowInfo("GeoSite", "Every source must be an absolute HTTP or HTTPS URL.", InfoBarSeverity.Error); + return; + } + + bool changed = false; + foreach (string source in parsed) + { + if (!_sourceItems.Any(existing => string.Equals(existing, source, StringComparison.OrdinalIgnoreCase))) + { + _sourceItems.Add(source); + changed = true; + } + } + + if (changed) + { + SetSourcesDirty(true); + } + _newSources.Text = string.Empty; + RebuildSourceList(_sourceItems.Count - 1); + } + + private void OnRemoveSelected(object _, RoutedEventArgs _1) + { + if (!IsLocalPacMode()) + { + return; + } + + int index = _sources.SelectedIndex; + if (index < 0) + { + return; + } + + if (_sourceItems.Count <= 1) + { + _context.ShowInfo("GeoSite", "At least one GeoSite source is required.", InfoBarSeverity.Warning); + return; + } + + _sourceItems.RemoveAt(index); + SetSourcesDirty(true); + RebuildSourceList(Math.Min(index, _sourceItems.Count - 1)); + } + + private void MoveSelected(int delta) + { + if (!IsLocalPacMode()) + { + return; + } + + int index = _sources.SelectedIndex; + int target = index + delta; + if (index < 0 || target < 0 || target >= _sourceItems.Count) + { + return; + } + + string value = _sourceItems[index]; + _sourceItems.RemoveAt(index); + _sourceItems.Insert(target, value); + SetSourcesDirty(true); + RebuildSourceList(target); + } + + private void OnRestoreDefault(object _, RoutedEventArgs _1) + { + if (!IsLocalPacMode()) + { + return; + } + + _sourceItems.Clear(); + _sourceItems.Add(GeositeUpdater.DefaultSourceUrl); + SetSourcesDirty(true); + RebuildSourceList(0); + } + + private void OnSaveSources(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || !IsLocalPacMode()) + { + return; + } + + _context.Controller.SaveGeositeSources(_sourceItems.ToList()); + SetSourcesDirty(false); + _context.ShowInfo("GeoSite", "GeoSite source list saved.", InfoBarSeverity.Success); + } + + private async void OnRefreshClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || !IsLocalPacMode() || _geoSiteRefreshRunning) + { + return; + } + + SetGeoSiteRefreshBusy(true); + try + { + bool updated = await _context.Controller.UpdatePACFromGeositeAsync(); + _context.ShowInfo( + "GeoSite", + updated ? "GeoSite data refreshed and Local PAC was regenerated." : "GeoSite refresh did not complete successfully.", + updated ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + catch (Exception exception) + { + _context.ShowInfo("GeoSite", exception.Message, InfoBarSeverity.Error); + } + finally + { + SetGeoSiteRefreshBusy(false); + } + } + + private void ApplyPacModeState(bool useOnlinePac) + { + // Do not leave scenario-specific PAC actions as disabled visual noise. + // Online PAC hides Local managed-routing diagnostics, user-rule.txt editing + // and GeoSite sources; Local PAC restores those local routing controls. + Visibility onlineVisibility = useOnlinePac ? Visibility.Visible : Visibility.Collapsed; + Visibility localVisibility = useOnlinePac ? Visibility.Collapsed : Visibility.Visible; + + _onlinePacOptions.Visibility = onlineVisibility; + _localPacOptions.Visibility = localVisibility; + _routingCard.Visibility = localVisibility; + _userRulesCard.Visibility = localVisibility; + _sourceCard.Visibility = localVisibility; + + UpdatePacActionState(); + UpdateSourceActionState(); + } + + private void UpdatePacActionState() + { + string candidate = _pacUrl.Text?.Trim() ?? string.Empty; + _savePacUrlButton.IsEnabled = IsValidPacUrl(candidate) + && !string.Equals(candidate, _savedPacUrl, StringComparison.Ordinal); + } + + private void UpdateSourceActionState() + { + bool idle = !_geoSiteRefreshRunning; + int index = _sources.SelectedIndex; + _sources.IsEnabled = idle; + _newSources.IsEnabled = idle; + _moveSourceUpButton.IsEnabled = idle && index > 0; + _moveSourceDownButton.IsEnabled = idle && index >= 0 && index < _sourceItems.Count - 1; + _removeSourceButton.IsEnabled = idle && index >= 0 && _sourceItems.Count > 1; + _restoreDefaultButton.IsEnabled = idle && (_sourceItems.Count != 1 + || !string.Equals(_sourceItems[0], GeositeUpdater.DefaultSourceUrl, StringComparison.OrdinalIgnoreCase)); + + List pending = ParseSources(_newSources.Text); + _addSourceButton.IsEnabled = idle && pending.Count > 0 && pending.All(IsValidSource); + _saveSourcesButton.IsEnabled = idle && _sourcesDirty; + _refreshGeoSiteButton.IsEnabled = idle && !_sourcesDirty; + _sourcePendingNote.Visibility = _sourcesDirty ? Visibility.Visible : Visibility.Collapsed; + } + + private void SetGeoSiteRefreshBusy(bool busy) + { + _geoSiteRefreshRunning = busy; + _geoSiteProgress.IsActive = busy; + _geoSiteProgress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + UpdateSourceActionState(); + } + + private void SetSourcesDirty(bool dirty) + { + _sourcesDirty = dirty; + UpdateSourceActionState(); + } + + private void SetOnlinePacToggleWithoutEvent(bool isOn) + { + _refreshing = true; + try + { + _onlinePacToggle.IsOn = isOn; + } + finally + { + _refreshing = false; + } + } + + private bool IsLocalPacMode() + => _context.Controller is not null + && !_context.Controller.GetCurrentConfiguration().useOnlinePac; + + private async Task PromptForOnlinePacUrlAsync() + { + XamlRoot? xamlRoot = _context.GetXamlRoot(); + if (xamlRoot is null) + { + _context.ShowInfo("PAC", "Please input PAC URL before enabling Online PAC.", InfoBarSeverity.Warning); + return null; + } + + while (true) + { + var input = new TextBox + { + Text = _pacUrl.Text?.Trim() ?? string.Empty, + MinWidth = 480, + PlaceholderText = "https://example.com/proxy.pac", + }; + input.SelectionStart = input.Text.Length; + _context.SetToolTip(input, "Enter an absolute HTTP or HTTPS PAC URL."); + + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _context.L("Edit Online PAC URL"), + Content = input, + PrimaryButtonText = _context.L("OK"), + CloseButtonText = _context.L("Cancel"), + DefaultButton = ContentDialogButton.Primary, + }; + + ContentDialogResult result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) + { + return null; + } + + string value = input.Text.Trim(); + if (IsValidPacUrl(value)) + { + return value; + } + + _context.ShowInfo("PAC", "Enter an absolute HTTP or HTTPS PAC URL.", InfoBarSeverity.Error); + } + } + + private static List ParseSources(string? value) + => (value ?? string.Empty) + .Split(SourceSeparators, StringSplitOptions.RemoveEmptyEntries) + .Select(source => source.Trim()) + .Where(source => !string.IsNullOrWhiteSpace(source)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static bool IsValidPacUrl(string? value) + => Uri.TryCreate(value?.Trim(), UriKind.Absolute, out Uri? uri) + && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp); + + private static bool IsValidSource(string source) + => Uri.TryCreate(source, UriKind.Absolute, out Uri? uri) + && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp); +} diff --git a/Shadowsocks.WinUI/Pages/PluginsPage.cs b/Shadowsocks.WinUI/Pages/PluginsPage.cs new file mode 100644 index 000000000..071b9f948 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/PluginsPage.cs @@ -0,0 +1,335 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller.Service; +using Shadowsocks.WinUI.UI; +using Windows.Storage; +using Windows.Storage.Pickers; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class PluginsPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly ComboBox _catalog; + private readonly Button _installButton; + private readonly Button _importButton; + private readonly Button _checkUpdatesButton; + private readonly ProgressRing _progress; + private readonly StackPanel _installedList; + private bool _busy; + + internal PluginsPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Plugins", + "Install SIP003 plugins from the built-in list or import a plugin ZIP or TAR.GZ package. Built-in catalog plugins can update automatically.", + out StackPanel panel); + + var addStack = new StackPanel { Spacing = 12 }; + addStack.Children.Add(WinUIStyles.CreateSectionTitle("Add plugin")); + + var addRow = new Grid { ColumnSpacing = 8 }; + addRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + addRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + addRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + addRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + addRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + _catalog = new ComboBox + { + HorizontalAlignment = HorizontalAlignment.Stretch, + PlaceholderText = "Choose a plugin", + }; + foreach (PluginCatalogEntry entry in PluginManager.Catalog) + { + _catalog.Items.Add(entry); + } + _catalog.DisplayMemberPath = nameof(PluginCatalogEntry.DisplayName); + _catalog.SelectedIndex = PluginManager.Catalog.Count > 0 ? 0 : -1; + _context.SetToolTip(_catalog, "Choose a supported Windows x64 SIP003 plugin to install."); + _catalog.SelectionChanged += (_, _) => UpdateControlState(); + addRow.Children.Add(_catalog); + + _installButton = new Button { Content = "Install", MinWidth = 88 }; + _context.SetToolTip(_installButton, "Download the latest Windows x64 release archive and install the selected plugin."); + _installButton.Click += async (_, _) => await InstallSelectedAsync(); + Grid.SetColumn(_installButton, 1); + addRow.Children.Add(_installButton); + + _importButton = new Button { Content = "Import ZIP/TAR.GZ", MinWidth = 132 }; + _context.SetToolTip(_importButton, "Import a local ZIP or TAR.GZ package containing a Windows plugin executable."); + _importButton.Click += async (_, _) => await ImportArchiveAsync(); + Grid.SetColumn(_importButton, 2); + addRow.Children.Add(_importButton); + + _checkUpdatesButton = new Button { Content = "Check updates", MinWidth = 112 }; + _context.SetToolTip(_checkUpdatesButton, "Check installed catalog plugins for newer GitHub releases now."); + _checkUpdatesButton.Click += async (_, _) => await CheckUpdatesAsync(); + Grid.SetColumn(_checkUpdatesButton, 3); + addRow.Children.Add(_checkUpdatesButton); + + _progress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + VerticalAlignment = VerticalAlignment.Center, + }; + Grid.SetColumn(_progress, 4); + addRow.Children.Add(_progress); + + addStack.Children.Add(addRow); + panel.Children.Add(WinUIStyles.CreateCard(addStack)); + + var installedStack = new StackPanel { Spacing = 12 }; + installedStack.Children.Add(WinUIStyles.CreateSectionTitle("Installed plugins")); + TextBlock maintenanceHint = WinUIStyles.CreateText( + "Built-in catalog plugins are checked automatically once per day when they are not in use.", + "CaptionTextBlockStyle"); + maintenanceHint.Opacity = 0.72; + installedStack.Children.Add(maintenanceHint); + _installedList = new StackPanel { Spacing = 8 }; + installedStack.Children.Add(_installedList); + panel.Children.Add(WinUIStyles.CreateCard(installedStack)); + + WinUILocalization.Apply(Content, _context.Localization); + Refresh(); + } + + public void Refresh() + { + _installedList.Children.Clear(); + InstalledPlugin[] installed = PluginManager.GetInstalledPlugins().ToArray(); + if (installed.Length == 0) + { + TextBlock empty = WinUIStyles.CreateText(_context.L("No plugins installed.")); + empty.Opacity = 0.72; + _installedList.Children.Add(empty); + UpdateControlState(); + return; + } + + foreach (InstalledPlugin plugin in installed) + { + var row = new Grid { ColumnSpacing = 12 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var text = new StackPanel { Spacing = 2 }; + text.Children.Add(WinUIStyles.CreateText(plugin.DisplayName, "BodyStrongTextBlockStyle")); + TextBlock source = WinUIStyles.CreateText(plugin.Source, "CaptionTextBlockStyle"); + source.Opacity = 0.72; + text.Children.Add(source); + if (!string.IsNullOrWhiteSpace(plugin.ReleaseTag)) + { + TextBlock release = WinUIStyles.CreateText(_context.LF("Release: {0}", plugin.ReleaseTag), "CaptionTextBlockStyle"); + release.Opacity = 0.72; + text.Children.Add(release); + } + row.Children.Add(text); + + if (plugin.CanAutoUpdate) + { + var automaticUpdates = new ToggleSwitch + { + Header = _context.L("Automatic updates"), + IsOn = plugin.AutoUpdate, + Tag = plugin.Id, + VerticalAlignment = VerticalAlignment.Center, + }; + _context.SetToolTip(automaticUpdates, "Automatically check this catalog plugin for a newer release once per day."); + automaticUpdates.Toggled += OnAutoUpdateToggled; + Grid.SetColumn(automaticUpdates, 1); + row.Children.Add(automaticUpdates); + } + + var remove = new Button { Content = _context.L("Remove"), Tag = plugin.Id, VerticalAlignment = VerticalAlignment.Center }; + _context.SetToolTip(remove, "Remove this installed plugin from Shadowsocks storage."); + remove.Click += OnRemoveClicked; + Grid.SetColumn(remove, 2); + row.Children.Add(remove); + _installedList.Children.Add(row); + } + UpdateControlState(); + } + + private async Task InstallSelectedAsync() + { + if (_catalog.SelectedItem is not PluginCatalogEntry entry) + return; + + SetBusy(true); + try + { + InstalledPlugin installed = await PluginManager.InstallCatalogPluginAsync(entry); + _context.ShowInfo(_context.L("Plugins"), _context.LF("{0} installed.", installed.DisplayName), InfoBarSeverity.Success); + Refresh(); + } + catch (Exception exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + } + finally + { + SetBusy(false); + } + } + + private async Task ImportArchiveAsync() + { + var picker = new FileOpenPicker + { + ViewMode = PickerViewMode.List, + SuggestedStartLocation = PickerLocationId.Downloads, + }; + picker.FileTypeFilter.Add(".zip"); + picker.FileTypeFilter.Add(".gz"); + picker.FileTypeFilter.Add(".tgz"); + + nint windowHandle = _context.GetWindowHandle(); + if (windowHandle != nint.Zero) + { + WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle); + } + + StorageFile? file = await picker.PickSingleFileAsync(); + if (file is null) + return; + + SetBusy(true); + try + { + InstalledPlugin installed = await Task.Run(() => PluginManager.InstallManualArchive(file.Path)); + _context.ShowInfo(_context.L("Plugins"), _context.LF("{0} installed.", installed.DisplayName), InfoBarSeverity.Success); + Refresh(); + } + catch (Exception exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + } + finally + { + SetBusy(false); + } + } + + private async Task CheckUpdatesAsync() + { + if (_context.Controller is null) + return; + + SetBusy(true); + try + { + PluginUpdateSummary summary = await _context.Controller.CheckPluginUpdatesAsync(force: true); + Refresh(); + + if (summary.Errors.Count > 0) + { + _context.ShowInfo(_context.L("Plugins"), string.Join(Environment.NewLine, summary.Errors), InfoBarSeverity.Error); + } + else if (summary.UpdatedCount > 0) + { + _context.ShowInfo(_context.L("Plugins"), _context.LF("{0} plugin(s) updated.", summary.UpdatedCount), InfoBarSeverity.Success); + } + else if (summary.SkippedInUseCount > 0) + { + _context.ShowInfo(_context.L("Plugins"), _context.L("Plugin updates were deferred because one or more plugins are in use."), InfoBarSeverity.Informational); + } + else + { + _context.ShowInfo(_context.L("Plugins"), _context.L("Installed catalog plugins are up to date."), InfoBarSeverity.Success); + } + } + catch (Exception exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + } + finally + { + SetBusy(false); + } + } + + private void OnAutoUpdateToggled(object sender, RoutedEventArgs _) + { + if (sender is not ToggleSwitch { Tag: string pluginId } automaticUpdates) + return; + + try + { + if (!PluginManager.SetAutoUpdate(pluginId, automaticUpdates.IsOn)) + Refresh(); + } + catch (IOException exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + Refresh(); + } + catch (UnauthorizedAccessException exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + Refresh(); + } + } + + private void OnRemoveClicked(object sender, RoutedEventArgs _) + { + if (sender is not Button { Tag: string pluginId }) + return; + + try + { + PluginManager.Remove(pluginId); + _context.ShowInfo(_context.L("Plugins"), _context.L("Plugin removed."), InfoBarSeverity.Success); + Refresh(); + } + catch (IOException exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + } + catch (UnauthorizedAccessException exception) + { + _context.ShowInfo(_context.L("Plugins"), exception.Message, InfoBarSeverity.Error); + } + } + + private void SetBusy(bool busy) + { + _busy = busy; + _progress.IsActive = busy; + _progress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + UpdateControlState(); + } + + private void UpdateControlState() + { + bool idle = !_busy; + InstalledPlugin[] installed = PluginManager.GetInstalledPlugins().ToArray(); + PluginCatalogEntry? selected = _catalog.SelectedItem as PluginCatalogEntry; + bool selectedInstalled = selected is not null + && installed.Any(plugin => string.Equals(plugin.Id, selected.Id, StringComparison.OrdinalIgnoreCase)); + + _catalog.IsEnabled = idle; + _installButton.Content = _context.L(selectedInstalled ? "Reinstall" : "Install"); + _context.SetToolTip( + _installButton, + selectedInstalled + ? "Download the latest Windows x64 release archive and replace the installed selected plugin." + : "Download the latest Windows x64 release archive and install the selected plugin."); + _installButton.IsEnabled = idle && selected is not null; + _importButton.IsEnabled = idle; + _checkUpdatesButton.IsEnabled = idle && installed.Any(plugin => plugin.CanAutoUpdate); + _installedList.IsHitTestVisible = idle; + _installedList.Opacity = idle ? 1.0 : 0.65; + } +} diff --git a/Shadowsocks.WinUI/Pages/ServersPage.cs b/Shadowsocks.WinUI/Pages/ServersPage.cs new file mode 100644 index 000000000..ae7981fa8 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/ServersPage.cs @@ -0,0 +1,997 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Model; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class ServersPage : Page, IRefreshablePage +{ + private sealed record PluginChoice(string Id, string DisplayName) + { + public override string ToString() => DisplayName; + } + + private static readonly string[] SupportedMethods = + [ + "none", + "plain", + "aes-256-gcm", + "aes-192-gcm", + "aes-128-gcm", + "chacha20-ietf-poly1305", + ]; + + private readonly WinUIPageContext _context; + private readonly ListView _serverList; + private readonly TextBox _host; + private readonly NumberBox _serverPort; + private readonly PasswordBox _password; + private readonly CheckBox _showPassword; + private readonly ComboBox _method; + private readonly ComboBox _plugin; + private readonly TextBox _pluginOptions; + private readonly TextBlock _pluginOptionsLabel; + private readonly CheckBox _pluginArgumentsEnabled; + private readonly TextBox _pluginArguments; + private readonly TextBlock _pluginArgumentsLabel; + private readonly TextBox _remarks; + private readonly NumberBox _timeout; + private readonly TextBox _group; + private readonly NumberBox _localPort; + private readonly Button _deleteButton; + private readonly Button _duplicateButton; + private readonly Button _shareButton; + private readonly Button _moveUpButton; + private readonly Button _moveDownButton; + private readonly Button _applyButton; + private readonly Button _discardButton; + private readonly Border _editorCard; + private readonly TextBlock _emptyServersText; + private readonly List _servers = new(); + private readonly HashSet _loadedServerIdentities = new(StringComparer.Ordinal); + private int _selectedIndex = -1; + private bool _loading; + private bool _loadedOnce; + private bool _dirty; + private bool _selectionValidationPending; + private readonly Dictionary _serverCountryFlags = new(StringComparer.Ordinal); + private int _countryLookupGeneration; + + internal ServersPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Servers", + "Manage proxy servers and local client settings.", + out StackPanel panel); + + var workspace = new Grid + { + ColumnSpacing = 16, + MinHeight = 540, + HorizontalAlignment = HorizontalAlignment.Left, + MaxWidth = 820, + }; + workspace.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(224) }); + workspace.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(500) }); + + var left = new StackPanel { Spacing = 8 }; + _serverList = new ListView + { + SelectionMode = ListViewSelectionMode.Single, + MinHeight = 430, + MaxHeight = 520, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }; + _serverList.SelectionChanged += OnServerSelectionChanged; + _context.SetToolTip(_serverList, "Select a server to edit, duplicate, share, move, or delete it."); + left.Children.Add(_serverList); + _emptyServersText = WinUIStyles.CreateText( + "No servers configured. Select Add to create one.", + "CaptionTextBlockStyle"); + _emptyServersText.Opacity = 0.72; + _emptyServersText.Visibility = Visibility.Collapsed; + left.Children.Add(_emptyServersText); + + // Match the original Config form: two compact button columns beneath the server list. + var listButtons = new Grid { ColumnSpacing = 6, RowSpacing = 6 }; + listButtons.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + listButtons.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + for (int row = 0; row < 4; row++) + { + listButtons.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + } + + var add = CreateListButton("Add", OnAddClicked); + _context.SetToolTip(add, "Create a new server entry."); + listButtons.Children.Add(add); + _deleteButton = CreateListButton("Delete", OnRemoveClicked); + _context.SetToolTip(_deleteButton, "Delete the selected server."); + Grid.SetColumn(_deleteButton, 1); + listButtons.Children.Add(_deleteButton); + + _duplicateButton = CreateListButton("Duplicate", OnDuplicateClicked); + _context.SetToolTip(_duplicateButton, "Create a copy of the selected server."); + Grid.SetRow(_duplicateButton, 1); + listButtons.Children.Add(_duplicateButton); + _shareButton = CreateListButton("Share Server Config", OnShareClicked); + _context.SetToolTip(_shareButton, "Open Share / QR for the selected server."); + Grid.SetRow(_shareButton, 3); + Grid.SetColumnSpan(_shareButton, 2); + listButtons.Children.Add(_shareButton); + + _moveUpButton = CreateListButton("Move up", (_, _) => MoveSelected(-1)); + _context.SetToolTip(_moveUpButton, "Move the selected server one position up."); + Grid.SetRow(_moveUpButton, 2); + listButtons.Children.Add(_moveUpButton); + _moveDownButton = CreateListButton("Move down", (_, _) => MoveSelected(1)); + _context.SetToolTip(_moveDownButton, "Move the selected server one position down."); + Grid.SetRow(_moveDownButton, 2); + Grid.SetColumn(_moveDownButton, 1); + listButtons.Children.Add(_moveDownButton); + left.Children.Add(listButtons); + + Border leftCard = WinUIStyles.CreateCard(left); + leftCard.Padding = new Thickness(12); + Grid.SetColumn(leftCard, 0); + workspace.Children.Add(leftCard); + + var right = new StackPanel { Spacing = 12 }; + + var editorStack = new StackPanel { Spacing = 10 }; + editorStack.Children.Add(WinUIStyles.CreateSectionTitle("Server")); + var form = CreateFormGrid(); + + _remarks = new TextBox { MaxLength = 128, PlaceholderText = "Optional display name" }; + _context.SetToolTip(_remarks, "Friendly name shown in server lists. If left empty, Shadowsocks assigns a unique Server N name."); + AddFormRow(form, "Server Name", _remarks); + _host = new TextBox { MaxLength = 512, PlaceholderText = "example.com or 203.0.113.10" }; + _context.SetToolTip(_host, "Hostname or IP address of the Shadowsocks server."); + AddFormRow(form, "Server IP", _host); + _serverPort = CreateNumberBox(1, 65535, Server.DefaultPort); + _context.SetToolTip(_serverPort, "TCP/UDP port exposed by the Shadowsocks server."); + AddFormRow(form, "Server Port", _serverPort); + _password = new PasswordBox { MaxLength = 256, PasswordRevealMode = PasswordRevealMode.Hidden }; + _context.SetToolTip(_password, "Password used by this Shadowsocks server configuration."); + AddFormRow(form, "Password", _password); + _showPassword = new CheckBox { Content = "Show Password" }; + _context.SetToolTip(_showPassword, "Temporarily reveal the password for manually managed servers. Link-managed servers keep this option hidden."); + _showPassword.Checked += (_, _) => _password.PasswordRevealMode = PasswordRevealMode.Visible; + _showPassword.Unchecked += (_, _) => _password.PasswordRevealMode = PasswordRevealMode.Hidden; + AddFormRow(form, string.Empty, _showPassword); + _method = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; + foreach (string method in SupportedMethods) + { + _method.Items.Add(method); + } + _context.SetToolTip(_method, "Encryption method negotiated with the selected Shadowsocks server."); + AddFormRow(form, "Encryption", _method); + _plugin = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; + _context.SetToolTip(_plugin, "Optional SIP003 plugin installed on the Plugins page."); + RefreshPluginChoices(null); + AddFormRow(form, "Plugin", _plugin); + _pluginOptions = new TextBox { MaxLength = 256 }; + _context.SetToolTip(_pluginOptions, "Options passed to the SIP003 plugin through SS_PLUGIN_OPTIONS."); + _pluginOptionsLabel = AddFormRow(form, "Plugin Options", _pluginOptions)!; + _pluginArgumentsEnabled = new CheckBox { Content = "Need Plugin Argument" }; + _context.SetToolTip(_pluginArgumentsEnabled, "Enable an additional command-line argument string for the plugin."); + _pluginArgumentsEnabled.Checked += (_, _) => + { + UpdatePluginFieldsVisibility(); + MarkDirty(); + }; + _pluginArgumentsEnabled.Unchecked += (_, _) => + { + UpdatePluginFieldsVisibility(); + MarkDirty(); + }; + AddFormRow(form, string.Empty, _pluginArgumentsEnabled); + _pluginArguments = new TextBox { MaxLength = 512 }; + _context.SetToolTip(_pluginArguments, "Additional command-line arguments passed directly to the plugin process."); + _pluginArgumentsLabel = AddFormRow(form, "Plugin Arguments", _pluginArguments)!; + _timeout = CreateNumberBox(1, Server.MaxServerTimeoutSec, 5); + _context.SetToolTip(_timeout, "Connection timeout for this server, in seconds."); + AddFormRow(form, "Timeout (Sec)", _timeout); + _group = new TextBox { IsReadOnly = true, MaxLength = 64 }; + _context.SetToolTip(_group, "Read-only group supplied by an imported or online configuration, when available."); + AddFormRow(form, "Group", _group); + editorStack.Children.Add(form); + _editorCard = WinUIStyles.CreateCard(editorStack); + _editorCard.Padding = new Thickness(16, 14, 16, 14); + right.Children.Add(_editorCard); + + var localStack = new StackPanel { Spacing = 10 }; + localStack.Children.Add(WinUIStyles.CreateSectionTitle("Local client")); + var localGrid = CreateFormGrid(); + _localPort = CreateNumberBox(1, 65535, 1080); + _context.SetToolTip(_localPort, "Local SOCKS/HTTP listening port used by Shadowsocks."); + AddFormRow(localGrid, "Proxy Port", _localPort); + localStack.Children.Add(localGrid); + + var actions = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 8, + }; + _discardButton = new Button { Content = "Discard changes", MinWidth = 112, IsEnabled = false }; + _context.SetToolTip(_discardButton, "Restore the selected server fields to their last saved values."); + _discardButton.Click += (_, _) => DiscardChanges(); + actions.Children.Add(_discardButton); + _applyButton = new Button { Content = "Apply", MinWidth = 88, IsEnabled = false }; + _context.SetToolTip(_applyButton, "Validate and save the current server and local client settings."); + _applyButton.Click += OnSaveClicked; + actions.Children.Add(_applyButton); + localStack.Children.Add(actions); + Border localCard = WinUIStyles.CreateCard(localStack); + localCard.Padding = new Thickness(16, 14, 16, 14); + right.Children.Add(localCard); + + Grid.SetColumn(right, 1); + workspace.Children.Add(right); + panel.Children.Add(workspace); + + _host.TextChanged += (_, _) => MarkDirty(); + _serverPort.ValueChanged += (_, _) => MarkDirty(); + _password.PasswordChanged += (_, _) => MarkDirty(); + _method.SelectionChanged += (_, _) => MarkDirty(); + _plugin.SelectionChanged += (_, _) => + { + UpdatePluginFieldsVisibility(); + MarkDirty(); + }; + _pluginOptions.TextChanged += (_, _) => MarkDirty(); + _pluginArguments.TextChanged += (_, _) => MarkDirty(); + _remarks.TextChanged += (_, _) => MarkDirty(); + _timeout.ValueChanged += (_, _) => MarkDirty(); + _localPort.ValueChanged += (_, _) => MarkDirty(); + UpdatePluginFieldsVisibility(); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + RefreshPluginChoices((_plugin.SelectedItem as PluginChoice)?.Id); + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + if (_loadedOnce && _dirty) + { + if (configuration is not null) + { + MergeExternallyAddedServers(configuration); + } + return; + } + + if (configuration is null) + { + return; + } + + _servers.Clear(); + _loadedServerIdentities.Clear(); + foreach (Server server in configuration.configs ?? []) + { + _servers.Add(CloneServer(server)); + _loadedServerIdentities.Add(BuildServerIdentity(server)); + } + _loading = true; + try + { + _localPort.Value = configuration.localPort; + } + finally + { + _loading = false; + } + + _selectedIndex = _servers.Count == 0 + ? -1 + : Math.Clamp(configuration.index, 0, _servers.Count - 1); + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + _loadedOnce = true; + SetDirty(false); + } + + private static Button CreateListButton(string text, RoutedEventHandler handler) + { + var button = new Button + { + Content = text, + HorizontalAlignment = HorizontalAlignment.Stretch, + HorizontalContentAlignment = HorizontalAlignment.Center, + }; + button.Click += handler; + return button; + } + + private static Grid CreateFormGrid() + { + var grid = new Grid { ColumnSpacing = 12, RowSpacing = 6 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(142) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + return grid; + } + + private static TextBlock? AddFormRow(Grid grid, string label, FrameworkElement control) + { + int row = grid.RowDefinitions.Count; + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + TextBlock? text = null; + if (!string.IsNullOrEmpty(label)) + { + text = WinUIStyles.CreateText(label); + text.VerticalAlignment = VerticalAlignment.Center; + text.HorizontalAlignment = HorizontalAlignment.Right; + text.TextAlignment = TextAlignment.Right; + Grid.SetRow(text, row); + grid.Children.Add(text); + } + control.HorizontalAlignment = HorizontalAlignment.Stretch; + Grid.SetRow(control, row); + Grid.SetColumn(control, 1); + grid.Children.Add(control); + return text; + } + + private static NumberBox CreateNumberBox(double minimum, double maximum, double value) + => new() + { + Minimum = minimum, + Maximum = maximum, + Value = value, + SmallChange = 1, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + }; + + private void RebuildServerList(int selectedIndex) + { + _loading = true; + try + { + _serverList.Items.Clear(); + for (int index = 0; index < _servers.Count; index++) + { + Server server = _servers[index]; + _serverList.Items.Add(new ListViewItem + { + Content = CreateServerListContent(server, index), + Padding = new Thickness(8, 5, 8, 5), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }); + } + _serverList.SelectedIndex = _servers.Count == 0 + ? -1 + : Math.Clamp(selectedIndex, 0, _servers.Count - 1); + _emptyServersText.Visibility = _servers.Count == 0 ? Visibility.Visible : Visibility.Collapsed; + } + finally + { + _loading = false; + } + UpdateListButtons(); + _ = RefreshServerCountryFlagsAsync(++_countryLookupGeneration); + } + + + private void MergeExternallyAddedServers(Configuration configuration) + { + bool added = false; + foreach (Server server in configuration.configs ?? []) + { + string identity = BuildServerIdentity(server); + if (_loadedServerIdentities.Contains(identity)) + { + continue; + } + + if (!_servers.Any(existing => string.Equals(BuildServerIdentity(existing), identity, StringComparison.Ordinal))) + { + _servers.Add(CloneServer(server)); + added = true; + } + + _loadedServerIdentities.Add(identity); + } + + if (added) + { + RebuildServerList(Math.Clamp(_selectedIndex, 0, _servers.Count - 1)); + } + } + + private static string BuildServerIdentity(Server server) + => string.Join("\u001F", + server.server ?? string.Empty, + server.ServerPort.ToString(System.Globalization.CultureInfo.InvariantCulture), + server.method ?? string.Empty, + server.password ?? string.Empty, + server.plugin ?? string.Empty, + server.PluginOptions ?? string.Empty, + server.remarks ?? string.Empty); + + private void UpdateServerListItem(int index) + { + if (index < 0 || index >= _servers.Count || index >= _serverList.Items.Count) + { + return; + } + + if (_serverList.Items[index] is ListViewItem item) + { + Server server = _servers[index]; + item.Content = CreateServerListContent(server, index); + } + } + + private Grid CreateServerListContent(Server server, int index) + { + string name = server.IsConfigured + ? !string.IsNullOrWhiteSpace(server.remarks) + ? server.remarks.Trim() + : _context.LF("Server {0}", index + 1) + : _context.LF("New server {0}", index + 1); + string identity = BuildServerIdentity(server); + _serverCountryFlags.TryGetValue(identity, out string? flag); + + var row = new Grid { ColumnSpacing = 8 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var nameBlock = new TextBlock + { + Text = name, + TextTrimming = TextTrimming.CharacterEllipsis, + VerticalAlignment = VerticalAlignment.Center, + }; + Grid.SetColumn(nameBlock, 0); + row.Children.Add(nameBlock); + + var flagBlock = new TextBlock + { + Text = flag ?? string.Empty, + FontSize = 18, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + MinWidth = 24, + TextAlignment = TextAlignment.Right, + }; + Grid.SetColumn(flagBlock, 1); + row.Children.Add(flagBlock); + return row; + } + + private async Task RefreshServerCountryFlagsAsync(int generation) + { + if (_context.Controller is null) + return; + for (int index = 0; index < _servers.Count; index++) + { + if (generation != _countryLookupGeneration) + return; + Server server = _servers[index]; + if (!server.IsConfigured) + continue; + string identity = BuildServerIdentity(server); + if (_serverCountryFlags.ContainsKey(identity)) + continue; + try + { + IpCountryInfo? country = await ShadowsocksController.GetServerCountryAsync(server); + if (generation != _countryLookupGeneration) + return; + if (country is not null && !string.IsNullOrWhiteSpace(country.FlagEmoji)) + { + _serverCountryFlags[identity] = country.FlagEmoji; + UpdateServerListItem(index); + } + } + catch + { + // Country flags are best-effort presentation metadata and must never block server editing. + } + } + } + + private async void OnServerSelectionChanged(object _, SelectionChangedEventArgs _1) + { + if (_loading || _selectionValidationPending || _serverList.SelectedIndex < 0) + { + return; + } + + int requestedIndex = _serverList.SelectedIndex; + int previousIndex = _selectedIndex; + if (previousIndex < 0 || previousIndex >= _servers.Count || previousIndex == requestedIndex) + { + _selectedIndex = requestedIndex; + LoadSelectedIntoEditor(); + UpdateListButtons(); + return; + } + + _selectionValidationPending = true; + _serverList.IsEnabled = false; + try + { + CaptureEditorIntoSelected(); + Server current = _servers[previousIndex]; + + if (IsCompletelyBlank(current) && _servers.Count > 1) + { + bool discard = await ConfirmDiscardUnconfiguredServerAsync(); + if (!discard) + { + RestoreServerSelection(previousIndex); + return; + } + + _servers.RemoveAt(previousIndex); + SetDirty(true); + if (requestedIndex > previousIndex) + { + requestedIndex--; + } + requestedIndex = Math.Clamp(requestedIndex, 0, _servers.Count - 1); + _selectedIndex = requestedIndex; + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + return; + } + + try + { + Configuration.CheckServer(current); + } + catch (Exception exception) + { + RestoreServerSelection(previousIndex); + _context.ShowInfo( + "Auto save failed", + _context.LF("{0} Fix the current server before selecting another one.", exception.Message), + InfoBarSeverity.Error); + return; + } + + UpdateServerListItem(previousIndex); + _selectedIndex = requestedIndex; + LoadSelectedIntoEditor(); + UpdateListButtons(); + } + finally + { + _serverList.IsEnabled = true; + _selectionValidationPending = false; + } + } + + private async System.Threading.Tasks.Task ConfirmDiscardUnconfiguredServerAsync() + { + XamlRoot? xamlRoot = _context.GetXamlRoot(); + if (xamlRoot is null) + { + return false; + } + + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _context.L("Unconfigured server"), + Content = _context.L("This server has not been configured. Discard it and switch to the selected server?"), + PrimaryButtonText = _context.L("Discard"), + CloseButtonText = _context.L("Stay"), + DefaultButton = ContentDialogButton.Close, + }; + return await dialog.ShowAsync() == ContentDialogResult.Primary; + } + + private void RestoreServerSelection(int index) + { + _loading = true; + try + { + _serverList.SelectedIndex = index; + } + finally + { + _loading = false; + } + UpdateListButtons(); + } + + private bool ValidateCurrentServerForAction() + { + if (_selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + return true; + } + + try + { + CaptureEditorIntoSelected(); + Configuration.CheckServer(_servers[_selectedIndex]); + UpdateServerListItem(_selectedIndex); + return true; + } + catch (Exception exception) + { + _context.ShowInfo("Server configuration", exception.Message, InfoBarSeverity.Error); + return false; + } + } + + + private void RefreshPluginChoices(string? currentPlugin) + { + bool wasLoading = _loading; + _loading = true; + try + { + string selected = currentPlugin ?? string.Empty; + _plugin.Items.Clear(); + _plugin.Items.Add(new PluginChoice(string.Empty, _context.L("None"))); + foreach (InstalledPlugin plugin in PluginManager.GetInstalledPlugins()) + { + _plugin.Items.Add(new PluginChoice(plugin.Id, plugin.DisplayName)); + } + + if (!string.IsNullOrWhiteSpace(selected) + && !_plugin.Items.OfType().Any(item => string.Equals(item.Id, selected, StringComparison.OrdinalIgnoreCase))) + { + _plugin.Items.Add(new PluginChoice(selected, selected)); + } + + SelectPlugin(selected); + } + finally + { + _loading = wasLoading; + } + } + + private void SelectPlugin(string? plugin) + { + string value = plugin?.Trim() ?? string.Empty; + PluginChoice? match = _plugin.Items.OfType().FirstOrDefault(item => + string.Equals(item.Id, value, StringComparison.OrdinalIgnoreCase)); + if (match is null && !string.IsNullOrWhiteSpace(value)) + { + match = new PluginChoice(value, value); + _plugin.Items.Add(match); + } + _plugin.SelectedItem = match ?? _plugin.Items.OfType().FirstOrDefault(); + } + + private void LoadSelectedIntoEditor() + { + if (_selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + ClearEditor(); + return; + } + + _loading = true; + try + { + Server server = _servers[_selectedIndex]; + _host.Text = server.server ?? string.Empty; + _serverPort.Value = server.ServerPort > 0 ? server.ServerPort : Server.DefaultPort; + _password.Password = server.password ?? string.Empty; + _showPassword.IsChecked = false; + _password.PasswordRevealMode = PasswordRevealMode.Hidden; + _showPassword.Visibility = IsPasswordRevealAllowed(server) ? Visibility.Visible : Visibility.Collapsed; + _method.SelectedItem = SupportedMethods.Contains(server.method, StringComparer.OrdinalIgnoreCase) + ? server.method + : Server.DefaultMethod; + SelectPlugin(server.plugin); + _pluginOptions.Text = server.PluginOptions ?? string.Empty; + _pluginArguments.Text = server.PluginArguments ?? string.Empty; + _pluginArgumentsEnabled.IsChecked = !string.IsNullOrWhiteSpace(server.PluginArguments); + _remarks.Text = server.remarks ?? string.Empty; + _group.Text = server.group ?? string.Empty; + _timeout.Value = server.timeout > 0 ? server.timeout : 5; + UpdatePluginFieldsVisibility(); + } + finally + { + _loading = false; + } + } + + + private void ClearEditor() + { + _loading = true; + try + { + _host.Text = string.Empty; + _serverPort.Value = Server.DefaultPort; + _password.Password = string.Empty; + _showPassword.IsChecked = false; + _password.PasswordRevealMode = PasswordRevealMode.Hidden; + _showPassword.Visibility = Visibility.Visible; + _method.SelectedItem = Server.DefaultMethod; + SelectPlugin(string.Empty); + _pluginOptions.Text = string.Empty; + _pluginArguments.Text = string.Empty; + _pluginArgumentsEnabled.IsChecked = false; + _remarks.Text = string.Empty; + _group.Text = string.Empty; + _timeout.Value = 5; + UpdatePluginFieldsVisibility(); + } + finally + { + _loading = false; + } + } + + private bool HasSelectedPlugin() + => _plugin.SelectedItem is PluginChoice choice && !string.IsNullOrWhiteSpace(choice.Id); + + private void UpdatePluginFieldsVisibility() + { + bool hasPlugin = HasSelectedPlugin(); + Visibility pluginVisibility = hasPlugin ? Visibility.Visible : Visibility.Collapsed; + _pluginOptions.Visibility = pluginVisibility; + _pluginOptionsLabel.Visibility = pluginVisibility; + _pluginArgumentsEnabled.Visibility = pluginVisibility; + + Visibility argumentsVisibility = hasPlugin && _pluginArgumentsEnabled.IsChecked == true + ? Visibility.Visible + : Visibility.Collapsed; + _pluginArguments.Visibility = argumentsVisibility; + _pluginArgumentsLabel.Visibility = argumentsVisibility; + } + + private void CaptureEditorIntoSelected() + { + if (_loading || _selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + return; + } + + Server server = _servers[_selectedIndex]; + server.server = _host.Text?.Trim() ?? string.Empty; + server.ServerPort = double.IsNaN(_serverPort.Value) ? Server.DefaultPort : checked((int)_serverPort.Value); + server.password = _password.Password ?? string.Empty; + server.method = _method.SelectedItem?.ToString() ?? Server.DefaultMethod; + server.plugin = (_plugin.SelectedItem as PluginChoice)?.Id ?? string.Empty; + bool hasPlugin = !string.IsNullOrWhiteSpace(server.plugin); + server.PluginOptions = hasPlugin ? (_pluginOptions.Text?.Trim() ?? string.Empty) : string.Empty; + server.PluginArguments = hasPlugin && _pluginArgumentsEnabled.IsChecked == true + ? (_pluginArguments.Text?.Trim() ?? string.Empty) + : string.Empty; + server.remarks = _remarks.Text?.Trim() ?? string.Empty; + server.timeout = double.IsNaN(_timeout.Value) ? 5 : checked((int)_timeout.Value); + } + + private void MarkDirty() + { + if (!_loading) + { + SetDirty(true); + } + } + + private void SetDirty(bool dirty) + { + _dirty = dirty; + _applyButton.IsEnabled = dirty; + _discardButton.IsEnabled = dirty; + } + + private void OnAddClicked(object _, RoutedEventArgs _1) + { + if (!ValidateCurrentServerForAction()) + { + return; + } + _servers.Add(new Server()); + SetDirty(true); + _selectedIndex = _servers.Count - 1; + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + } + + private void OnDuplicateClicked(object _, RoutedEventArgs _1) + { + if (!ValidateCurrentServerForAction()) + { + return; + } + if (_selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + return; + } + + _servers.Insert(_selectedIndex + 1, CloneServer(_servers[_selectedIndex])); + SetDirty(true); + _selectedIndex++; + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + } + + + private void OnShareClicked(object _, RoutedEventArgs _1) + { + if (_selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + return; + } + + if (_dirty && !SaveChanges(showSuccess: false)) + { + return; + } + + _context.NavigateToSharingServer(_selectedIndex); + } + + private void OnRemoveClicked(object _, RoutedEventArgs _1) + { + if (_selectedIndex < 0 || _selectedIndex >= _servers.Count) + { + return; + } + + _servers.RemoveAt(_selectedIndex); + SetDirty(true); + _selectedIndex = _servers.Count == 0 ? -1 : Math.Min(_selectedIndex, _servers.Count - 1); + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + } + + private void MoveSelected(int delta) + { + CaptureEditorIntoSelected(); + int target = _selectedIndex + delta; + if (_selectedIndex < 0 || target < 0 || target >= _servers.Count) + { + return; + } + + Server server = _servers[_selectedIndex]; + _servers.RemoveAt(_selectedIndex); + _servers.Insert(target, server); + SetDirty(true); + _selectedIndex = target; + RebuildServerList(_selectedIndex); + LoadSelectedIntoEditor(); + } + + private void UpdateListButtons() + { + bool hasSelection = _selectedIndex >= 0 && _selectedIndex < _servers.Count; + SetEditorEnabled(hasSelection); + _editorCard.Opacity = hasSelection ? 1.0 : 0.62; + _deleteButton.IsEnabled = hasSelection; + _duplicateButton.IsEnabled = hasSelection; + _shareButton.IsEnabled = hasSelection; + _moveUpButton.IsEnabled = _selectedIndex > 0; + _moveDownButton.IsEnabled = _selectedIndex >= 0 && _selectedIndex < _servers.Count - 1; + } + + private void SetEditorEnabled(bool enabled) + { + _remarks.IsEnabled = enabled; + _host.IsEnabled = enabled; + _serverPort.IsEnabled = enabled; + _password.IsEnabled = enabled; + _showPassword.IsEnabled = enabled; + _method.IsEnabled = enabled; + _plugin.IsEnabled = enabled; + _pluginOptions.IsEnabled = enabled; + _pluginArgumentsEnabled.IsEnabled = enabled; + _pluginArguments.IsEnabled = enabled; + _timeout.IsEnabled = enabled; + _group.IsEnabled = enabled; + } + + private void DiscardChanges() + { + SetDirty(false); + _loadedOnce = false; + Refresh(); + } + + private void OnSaveClicked(object _, RoutedEventArgs _1) + => SaveChanges(showSuccess: true); + + private bool SaveChanges(bool showSuccess) + { + if (_context.Controller is null) + { + return false; + } + + try + { + CaptureEditorIntoSelected(); + int localPort = double.IsNaN(_localPort.Value) ? 0 : checked((int)_localPort.Value); + Configuration.CheckLocalPort(localPort); + + var toSave = new List(); + int savedSelection = -1; + for (int index = 0; index < _servers.Count; index++) + { + Server server = _servers[index]; + if (IsCompletelyBlank(server)) + continue; + + if (index == _selectedIndex) + savedSelection = toSave.Count; + Server copy = CloneServer(server); + Configuration.CheckServer(copy); + toSave.Add(copy); + } + + _selectedIndex = toSave.Count == 0 + ? -1 + : savedSelection >= 0 ? savedSelection : 0; + _context.Controller.SaveServers(toSave, localPort); + _context.Controller.SelectServerIndex(_selectedIndex); + _context.Controller.CompleteFirstRun(); + if (showSuccess) + { + _context.ShowInfo("Servers", "Server configuration saved.", InfoBarSeverity.Success); + } + _loadedOnce = false; + SetDirty(false); + Refresh(); + return true; + } + catch (Exception exception) + { + _context.ShowInfo("Server configuration", exception.Message, InfoBarSeverity.Error); + return false; + } + } + + private static bool IsPasswordRevealAllowed(Server server) + { + if (server is null) + { + return true; + } + if (server.importedFromUrl) + { + return false; + } + return !Uri.TryCreate(server.group, UriKind.Absolute, out Uri? groupUri) + || (groupUri.Scheme != Uri.UriSchemeHttp && groupUri.Scheme != Uri.UriSchemeHttps); + } + + private static bool IsCompletelyBlank(Server server) + => string.IsNullOrWhiteSpace(server.server) + && string.IsNullOrWhiteSpace(server.password) + && string.IsNullOrWhiteSpace(server.remarks) + && string.IsNullOrWhiteSpace(server.plugin) + && string.IsNullOrWhiteSpace(server.group); + + private static Server CloneServer(Server source) + => new() + { + server = source.server ?? string.Empty, + ServerPort = source.ServerPort, + password = source.password ?? string.Empty, + method = source.method ?? Server.DefaultMethod, + plugin = source.plugin ?? string.Empty, + PluginOptions = source.PluginOptions ?? string.Empty, + PluginArguments = source.PluginArguments ?? string.Empty, + remarks = source.remarks ?? string.Empty, + group = source.group ?? string.Empty, + timeout = source.timeout, + importedFromUrl = source.importedFromUrl, + }; +} diff --git a/Shadowsocks.WinUI/Pages/SettingsPage.cs b/Shadowsocks.WinUI/Pages/SettingsPage.cs new file mode 100644 index 000000000..748f657f1 --- /dev/null +++ b/Shadowsocks.WinUI/Pages/SettingsPage.cs @@ -0,0 +1,181 @@ +using System; +using Shadowsocks.Core.Storage; +using System.IO; +using System.Diagnostics; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class SettingsPage : Page, IRefreshablePage +{ + private readonly ComboBox _themeBox; + private readonly ToggleSwitch _startupToggle; + private readonly ProgressRing _startupProgress; + private readonly TextBlock _storagePath; + private readonly WinUIPageContext _context; + private bool _refreshing; + private bool _startupChanging; + + internal SettingsPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Settings", + "Application preferences use native Windows controls and follow the Windows 11 settings layout.", + out StackPanel panel); + + var appearance = new StackPanel { Spacing = 12 }; + appearance.Children.Add(WinUIStyles.CreateSectionTitle("Appearance")); + appearance.Children.Add(WinUIStyles.CreateText("Theme follows Windows by default. Choose a fixed theme only when you explicitly want to override the system setting.")); + _themeBox = new ComboBox + { + Header = "App theme", + Width = 260, + HorizontalAlignment = HorizontalAlignment.Left, + }; + _themeBox.Items.Add(new ComboBoxItem { Content = "System", Tag = AppThemePreference.System }); + _themeBox.Items.Add(new ComboBoxItem { Content = "Light", Tag = AppThemePreference.Light }); + _themeBox.Items.Add(new ComboBoxItem { Content = "Dark", Tag = AppThemePreference.Dark }); + _themeBox.SelectionChanged += OnThemeChanged; + _context.SetToolTip(_themeBox, "Choose whether Shadowsocks follows the Windows theme or always uses light or dark mode."); + appearance.Children.Add(_themeBox); + panel.Children.Add(WinUIStyles.CreateCard(appearance)); + + var behavior = new StackPanel { Spacing = 16 }; + behavior.Children.Add(WinUIStyles.CreateSectionTitle("Behavior")); + _startupToggle = new ToggleSwitch + { + Header = "Start on Boot", + OnContent = "On", + OffContent = "Off", + }; + _startupToggle.Toggled += OnStartupToggled; + _context.SetToolTip(_startupToggle, "Start Shadowsocks automatically when you sign in to Windows. This option is unavailable in Clean Mode."); + _startupProgress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + VerticalAlignment = VerticalAlignment.Center, + }; + var startupRow = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12, + }; + startupRow.Children.Add(_startupToggle); + startupRow.Children.Add(_startupProgress); + behavior.Children.Add(startupRow); + if (AppStoragePaths.IsCleanMode) + { + behavior.Children.Add(WinUIStyles.CreateText("Start on Boot is unavailable in Clean Mode.", "CaptionTextBlockStyle")); + } + panel.Children.Add(WinUIStyles.CreateCard(behavior)); + + var storage = new StackPanel { Spacing = 12 }; + storage.Children.Add(WinUIStyles.CreateSectionTitle("Storage")); + storage.Children.Add(WinUIStyles.CreateText( + AppStoragePaths.IsCleanMode + ? "Clean Mode is active. All writable data is stored in a temporary session directory and removed when Shadowsocks exits." + : @"All writable application data is stored under %LOCALAPPDATA%\Shadowsocks.")); + _storagePath = WinUIStyles.CreateText(AppStoragePaths.StorageRoot, "CaptionTextBlockStyle"); + storage.Children.Add(_storagePath); + var openDataFolder = new Button + { + Content = "Open", + HorizontalAlignment = HorizontalAlignment.Left, + }; + openDataFolder.Click += OnOpenDataFolder; + _context.SetToolTip(openDataFolder, "Open the folder containing settings, logs, caches and runtime data for the current mode."); + storage.Children.Add(openDataFolder); + panel.Children.Add(WinUIStyles.CreateCard(storage)); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + _refreshing = true; + try + { + AppThemePreference currentTheme = _context?.GetTheme() ?? AppThemePreference.System; + foreach (object item in _themeBox.Items) + { + if (item is ComboBoxItem comboItem && comboItem.Tag is AppThemePreference theme && theme == currentTheme) + { + _themeBox.SelectedItem = comboItem; + break; + } + } + _startupToggle.IsEnabled = !AppStoragePaths.IsCleanMode; + _startupToggle.IsOn = !AppStoragePaths.IsCleanMode && _context?.GetStartWithWindows() == true; + _storagePath.Text = AppStoragePaths.StorageRoot; + } + finally + { + _refreshing = false; + } + } + + private void OnThemeChanged(object _, SelectionChangedEventArgs _1) + { + if (_refreshing || _context is null || _themeBox.SelectedItem is not ComboBoxItem { Tag: AppThemePreference theme }) + { + return; + } + + _context.SetTheme(theme); + } + + private async void OnStartupToggled(object _, Microsoft.UI.Xaml.RoutedEventArgs _1) + { + if (_refreshing || _startupChanging || _context is null || AppStoragePaths.IsCleanMode) + { + return; + } + + bool requested = _startupToggle.IsOn; + _startupChanging = true; + _startupToggle.IsEnabled = false; + _startupProgress.Visibility = Visibility.Visible; + _startupProgress.IsActive = true; + try + { + if (!await _context.SetStartWithWindows(requested)) + { + _context.ShowInfo("Startup", "Failed to update Start on Boot.", InfoBarSeverity.Error); + Refresh(); + } + } + finally + { + _startupProgress.IsActive = false; + _startupProgress.Visibility = Visibility.Collapsed; + _startupChanging = false; + _startupToggle.IsEnabled = !AppStoragePaths.IsCleanMode; + } + } + + private void OnOpenDataFolder(object _, RoutedEventArgs _1) + { + try + { + Directory.CreateDirectory(AppStoragePaths.StorageRoot); + Process.Start(new ProcessStartInfo + { + FileName = AppStoragePaths.StorageRoot, + UseShellExecute = true, + }); + } + catch (Exception) + { + _context.ShowInfo("Storage", "Failed to open data folder.", InfoBarSeverity.Error); + } + } + +} diff --git a/Shadowsocks.WinUI/Pages/SharingPage.cs b/Shadowsocks.WinUI/Pages/SharingPage.cs new file mode 100644 index 000000000..d1be854ac --- /dev/null +++ b/Shadowsocks.WinUI/Pages/SharingPage.cs @@ -0,0 +1,573 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; +using Shadowsocks.Windows.WinUI.Shell; +using Shadowsocks.WinUI.UI; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using Windows.Storage.Pickers; +using Windows.Storage.Streams; +using ZXing.QrCode.Internal; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class SharingPage : Page, IRefreshablePage +{ + private readonly WinUIPageContext _context; + private readonly ListView _servers; + private readonly TextBox _url; + private readonly Image _qrImage; + private readonly TextBox _importUrl; + private readonly Button _copyButton; + private readonly Button _importButton; + private Configuration? _configuration; + private bool _refreshing; + + internal SharingPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Share / QR", + "Share a server using the same list + QR workflow as the original client, or import ss:// links.", + out StackPanel panel); + + var shareGrid = new Grid { ColumnSpacing = 20, RowSpacing = 12 }; + shareGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(360) }); + shareGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + shareGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + shareGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + var qrHost = new Border + { + MinHeight = 340, + Padding = new Thickness(12), + Background = WinUIStyles.GetBrush("ControlFillColorDefaultBrush"), + CornerRadius = new CornerRadius(6), + }; + _qrImage = new Image + { + Width = 320, + Height = 320, + Stretch = Microsoft.UI.Xaml.Media.Stretch.Uniform, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }; + qrHost.Child = _qrImage; + shareGrid.Children.Add(qrHost); + + var serverHost = new StackPanel { Spacing = 8 }; + serverHost.Children.Add(WinUIStyles.CreateSectionTitle("Servers")); + _servers = new ListView + { + SelectionMode = ListViewSelectionMode.Single, + MinHeight = 340, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + _servers.SelectionChanged += OnServerSelectionChanged; + _context.SetToolTip(_servers, "Select a server to share, copy, or delete it."); + serverHost.Children.Add(_servers); + Grid.SetColumn(serverHost, 1); + shareGrid.Children.Add(serverHost); + + var urlGrid = new Grid { ColumnSpacing = 8 }; + urlGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + urlGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + _url = new TextBox + { + IsReadOnly = true, + TextWrapping = TextWrapping.NoWrap, + PlaceholderText = "ss://", + }; + _context.SetToolTip(_url, "The selected server's ss:// URL. Use Copy to place it on the clipboard."); + urlGrid.Children.Add(_url); + _copyButton = new Button { Content = "Copy", MinWidth = 92, IsEnabled = false }; + _context.SetToolTip(_copyButton, "Copy the selected server as an ss:// URL."); + _copyButton.Click += OnCopyClicked; + Grid.SetColumn(_copyButton, 1); + urlGrid.Children.Add(_copyButton); + Grid.SetRow(urlGrid, 1); + Grid.SetColumnSpan(urlGrid, 2); + shareGrid.Children.Add(urlGrid); + panel.Children.Add(WinUIStyles.CreateCard(shareGrid)); + + var importCard = new StackPanel { Spacing = 10 }; + importCard.Children.Add(WinUIStyles.CreateSectionTitle("Import server")); + _importUrl = new TextBox + { + Header = "ss:// link", + PlaceholderText = "Paste one or more ss:// links", + AcceptsReturn = true, + MinHeight = 74, + TextWrapping = TextWrapping.Wrap, + }; + _context.SetToolTip(_importUrl, "Paste one or more ss:// server links, one per line."); + _importUrl.TextChanged += (_, _) => UpdateActionState(); + importCard.Children.Add(_importUrl); + + var urlActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + _importButton = new Button { Content = "Import", HorizontalAlignment = HorizontalAlignment.Left, IsEnabled = false }; + _context.SetToolTip(_importButton, "Import every valid ss:// link from the text box."); + _importButton.Click += OnImportClicked; + urlActions.Children.Add(_importButton); + var pasteUrl = new Button { Content = "Paste URL from clipboard" }; + _context.SetToolTip(pasteUrl, "Paste text from the clipboard into the ss:// import field."); + pasteUrl.Click += OnPasteUrlClicked; + urlActions.Children.Add(pasteUrl); + importCard.Children.Add(urlActions); + + var qrActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + var scanScreen = new Button { Content = "Scan QRCode from Screen" }; + _context.SetToolTip(scanScreen, "Capture the screen and import the first supported Shadowsocks QR code found."); + scanScreen.Click += OnScanScreenClicked; + qrActions.Children.Add(scanScreen); + var openImage = new Button { Content = "Open image" }; + _context.SetToolTip(openImage, "Choose an image file and scan it for a Shadowsocks QR code."); + openImage.Click += OnOpenImageClicked; + qrActions.Children.Add(openImage); + var clipboardImage = new Button { Content = "Scan QR from clipboard image" }; + _context.SetToolTip(clipboardImage, "Scan the image currently stored in the clipboard for a Shadowsocks QR code."); + clipboardImage.Click += OnClipboardImageClicked; + qrActions.Children.Add(clipboardImage); + importCard.Children.Add(qrActions); + panel.Children.Add(WinUIStyles.CreateCard(importCard)); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Configuration? configuration = _context.Controller?.GetCurrentConfiguration(); + if (configuration?.configs is null) + { + return; + } + + _configuration = configuration; + _refreshing = true; + try + { + _servers.Items.Clear(); + for (int index = 0; index < configuration.configs.Count; index++) + { + Server server = configuration.configs[index]; + _servers.Items.Add(CreateServerListItem(server, index)); + } + if (_servers.Items.Count > 0) + { + _servers.SelectedIndex = Math.Clamp(configuration.index, 0, _servers.Items.Count - 1); + } + } + finally + { + _refreshing = false; + } + _ = UpdateSharedServerAsync(); + } + + public void SelectServer(int index) + { + Refresh(); + if (_servers.Items.Count > 0) + { + _servers.SelectedIndex = Math.Clamp(index, 0, _servers.Items.Count - 1); + } + } + + private ListViewItem CreateServerListItem(Server server, int index) + { + var row = new Grid { ColumnSpacing = 8 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var label = new TextBlock + { + Text = server.IsConfigured ? server.ToString() : _context.LF("Server {0}", index + 1), + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + }; + row.Children.Add(label); + + var delete = new Button + { + Content = new FontIcon + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uE74D", + FontSize = 11, + }, + Tag = index, + Width = 30, + Height = 30, + Padding = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Right, + }; + _context.SetToolTip(delete, "Delete the selected server."); + delete.Click += OnDeleteServerClicked; + Grid.SetColumn(delete, 1); + row.Children.Add(delete); + + return new ListViewItem + { + Content = row, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + Padding = new Thickness(8, 4, 4, 4), + }; + } + + public async Task ImportUrlAsync(string value) + { + if (_context.Controller is null) + { + return; + } + + value = value?.Trim() ?? string.Empty; + _importUrl.Text = value; + List servers = Server.GetServers(value); + if (servers.Count == 0) + { + _context.ShowInfo("Import", "No valid ss:// server links were found.", InfoBarSeverity.Error); + return; + } + + if (!await EnsureRequiredCatalogPluginsAsync(servers)) + { + return; + } + + XamlRoot? xamlRoot = _context.GetXamlRoot(); + if (xamlRoot is null) + { + return; + } + + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _context.L("Import server configuration?"), + Content = _context.L("Import from URL:") + "\n" + value, + PrimaryButtonText = _context.L("Import"), + CloseButtonText = _context.L("Cancel"), + DefaultButton = ContentDialogButton.Primary, + }; + if (await dialog.ShowAsync() != ContentDialogResult.Primary) + { + return; + } + + if (_context.Controller.AddServerBySSURL(value)) + { + _importUrl.Text = string.Empty; + _context.ShowInfo("Import", "Server configuration imported.", InfoBarSeverity.Success); + Refresh(); + } + else + { + _context.ShowInfo("Import", "Failed to import. Please check if the link is valid.", InfoBarSeverity.Error); + } + } + + private async Task EnsureRequiredCatalogPluginsAsync(List servers) + { + PluginCatalogEntry[] missingPlugins = servers + .Select(server => server.plugin?.Trim()) + .Where(plugin => !string.IsNullOrWhiteSpace(plugin)) + .Where(plugin => PluginManager.ResolveExecutable(plugin!) is null) + .Select(plugin => PluginManager.FindCatalogEntry(plugin!)) + .Where(entry => entry is not null) + .Cast() + .GroupBy(entry => entry.Id, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .ToArray(); + + if (missingPlugins.Length == 0) + { + return true; + } + + XamlRoot? xamlRoot = _context.GetXamlRoot(); + if (xamlRoot is null) + { + return false; + } + + string pluginNames = string.Join(", ", missingPlugins.Select(entry => entry.DisplayName)); + var dialog = new ContentDialog + { + XamlRoot = xamlRoot, + Title = _context.L("Required plugin is not installed"), + Content = new TextBlock + { + Text = _context.LF("This ss:// configuration requires {0}. The plugin is available in the built-in catalog but is not installed.", pluginNames), + TextWrapping = TextWrapping.Wrap, + }, + PrimaryButtonText = _context.L("Install"), + SecondaryButtonText = _context.L("Import without plugin"), + CloseButtonText = _context.L("Cancel"), + DefaultButton = ContentDialogButton.Primary, + }; + + ContentDialogResult result = await dialog.ShowAsync(); + if (result == ContentDialogResult.None) + { + return false; + } + if (result == ContentDialogResult.Secondary) + { + return true; + } + + foreach (PluginCatalogEntry entry in missingPlugins) + { + try + { + _context.ShowInfo( + _context.L("Plugins"), + _context.LF("Installing required plugin: {0}", entry.DisplayName), + InfoBarSeverity.Informational); + await PluginManager.InstallCatalogPluginAsync(entry); + } + catch (Exception exception) + { + _context.ShowInfo( + _context.L("Plugins"), + _context.LF("Failed to install required plugin {0}: {1}", entry.DisplayName, exception.Message), + InfoBarSeverity.Error); + return false; + } + } + + _context.ShowInfo( + _context.L("Plugins"), + _context.L("Required plugin installation completed."), + InfoBarSeverity.Success); + return true; + } + + private void OnDeleteServerClicked(object sender, RoutedEventArgs _1) + { + if (_context.Controller is null || sender is not Button { Tag: int index }) + { + return; + } + + if (_context.Controller.RemoveServerAt(index)) + { + _context.ShowInfo("Servers", "Server deleted.", InfoBarSeverity.Success); + Refresh(); + } + } + + private async void OnPasteUrlClicked(object _, RoutedEventArgs _1) + { + DataPackageView content = Clipboard.GetContent(); + if (!content.Contains(StandardDataFormats.Text)) + { + _context.ShowInfo("Import", "Clipboard does not contain text", InfoBarSeverity.Warning); + return; + } + + _importUrl.Text = (await content.GetTextAsync()).Trim(); + } + + private async void OnScanScreenClicked(object _, RoutedEventArgs _1) + => await HandleScannedValueAsync(ScreenQrCodeScanner.ScanFromScreen()); + + private async void OnOpenImageClicked(object _, RoutedEventArgs _1) + { + var picker = new FileOpenPicker + { + ViewMode = PickerViewMode.Thumbnail, + SuggestedStartLocation = PickerLocationId.PicturesLibrary, + }; + picker.FileTypeFilter.Add(".png"); + picker.FileTypeFilter.Add(".jpg"); + picker.FileTypeFilter.Add(".jpeg"); + picker.FileTypeFilter.Add(".bmp"); + picker.FileTypeFilter.Add(".gif"); + picker.FileTypeFilter.Add(".tif"); + picker.FileTypeFilter.Add(".tiff"); + + nint windowHandle = _context.GetWindowHandle(); + if (windowHandle != nint.Zero) + { + WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle); + } + + StorageFile? file = await picker.PickSingleFileAsync(); + if (file is null) + { + return; + } + + try + { + using var randomStream = await file.OpenReadAsync(); + using Stream stream = randomStream.AsStreamForRead(); + await HandleScannedValueAsync(ScreenQrCodeScanner.ScanFromStream(stream)); + } + catch (Exception exception) + { + _context.ShowInfo("QR code", exception.Message, InfoBarSeverity.Error); + } + } + + private async void OnClipboardImageClicked(object _, RoutedEventArgs _1) + { + try + { + (bool hasImage, string? result) = await ScreenQrCodeScanner.ScanFromClipboardAsync(); + if (!hasImage) + { + _context.ShowInfo("QR code", "Clipboard does not contain an image.", InfoBarSeverity.Warning); + return; + } + + await HandleScannedValueAsync(result); + } + catch (Exception exception) + { + _context.ShowInfo("QR code", exception.Message, InfoBarSeverity.Error); + } + } + + private async Task HandleScannedValueAsync(string? value) + { + value = value?.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + _context.ShowInfo("QR code", "No QRCode found. Try to zoom in or move it to the center of the screen.", InfoBarSeverity.Warning); + return; + } + + if (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + Process.Start(new ProcessStartInfo(value) { UseShellExecute = true }); + return; + } + + await ImportUrlAsync(value); + } + + private async void OnServerSelectionChanged(object _, SelectionChangedEventArgs _1) + { + if (!_refreshing) + { + await UpdateSharedServerAsync(); + } + } + + private async Task UpdateSharedServerAsync() + { + if (_configuration?.configs is null + || _servers.SelectedIndex < 0 + || _servers.SelectedIndex >= _configuration.configs.Count) + { + _url.Text = string.Empty; + _qrImage.Source = null; + UpdateActionState(); + return; + } + + Server server = _configuration.configs[_servers.SelectedIndex]; + if (!server.IsConfigured) + { + _url.Text = _context.L("Server is not configured."); + _qrImage.Source = null; + UpdateActionState(); + return; + } + + string url = server.GetURL(); + _url.Text = url; + UpdateActionState(); + try + { + _qrImage.Source = await CreateQrSvgSourceAsync(url); + } + catch (Exception exception) + { + _qrImage.Source = null; + _context.ShowInfo("QR code", exception.Message, InfoBarSeverity.Warning); + } + } + + private void UpdateActionState() + { + _copyButton.IsEnabled = _url.Text.StartsWith("ss://", StringComparison.OrdinalIgnoreCase); + _importButton.IsEnabled = ContainsPotentialSsUrl(_importUrl.Text); + } + + private static bool ContainsPotentialSsUrl(string? value) + => !string.IsNullOrWhiteSpace(value) + && value.Contains("ss://", StringComparison.OrdinalIgnoreCase); + + private void OnCopyClicked(object _, RoutedEventArgs _1) + { + if (string.IsNullOrWhiteSpace(_url.Text) || !_url.Text.StartsWith("ss://", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + var package = new DataPackage(); + package.SetText(_url.Text); + Clipboard.SetContent(package); + Clipboard.Flush(); + _context.ShowInfo("Share", "Server link copied to the clipboard.", InfoBarSeverity.Success); + } + + private async void OnImportClicked(object _, RoutedEventArgs _1) + => await ImportUrlAsync(_importUrl.Text ?? string.Empty); + + private static async Task CreateQrSvgSourceAsync(string text) + { + QRCode code = ZXing.QrCode.Internal.Encoder.encode(text, ErrorCorrectionLevel.L); + var matrix = code.Matrix; + const int quietZone = 4; + int width = matrix.Width + quietZone * 2; + int height = matrix.Height + quietZone * 2; + + var path = new StringBuilder(); + for (int y = 0; y < matrix.Height; y++) + { + for (int x = 0; x < matrix.Width; x++) + { + if (matrix[x, y] != 0) + { + path.Append('M').Append(x + quietZone).Append(' ').Append(y + quietZone).Append("h1v1h-1z"); + } + } + } + + string svg = $""; + using var stream = new InMemoryRandomAccessStream(); + using (var writer = new DataWriter(stream)) + { + writer.UnicodeEncoding = global::Windows.Storage.Streams.UnicodeEncoding.Utf8; + writer.WriteString(svg); + await writer.StoreAsync(); + await writer.FlushAsync(); + writer.DetachStream(); + } + stream.Seek(0); + var source = new SvgImageSource(); + SvgImageSourceLoadStatus status = await source.SetSourceAsync(stream); + if (status != SvgImageSourceLoadStatus.Success) + { + throw new InvalidOperationException($"WinUI could not decode the generated QR SVG ({status})."); + } + return source; + } +} diff --git a/Shadowsocks.WinUI/Pages/TrafficPage.cs b/Shadowsocks.WinUI/Pages/TrafficPage.cs new file mode 100644 index 000000000..4efaa789f --- /dev/null +++ b/Shadowsocks.WinUI/Pages/TrafficPage.cs @@ -0,0 +1,547 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Model; +using Shadowsocks.WinUI.UI; + +namespace Shadowsocks.WinUI.Pages; + +public sealed class TrafficPage : Page, IRefreshablePage +{ + private sealed class RuleEditor + { + public required Grid Root { get; init; } + public required CheckBox Enabled { get; init; } + public required TextBox Application { get; init; } + public required ComboBox Action { get; init; } + } + + private readonly WinUIPageContext _context; + private readonly ToggleSwitch _systemProxyToggle; + private readonly ToggleSwitch _shareLanToggle; + private readonly RadioButton _userMode; + private readonly RadioButton _adminMode; + private readonly ProgressRing _captureModeProgress; + private readonly NumberBox _localPortBox; + private readonly TextBlock _runtimeMode; + private readonly TextBlock _winDivertStatus; + private readonly TextBlock _networkServiceStatus; + private readonly TextBlock _tcpCaptureStatus; + private readonly TextBlock _udpCaptureStatus; + private readonly TextBlock _tcpRedirectPort; + private readonly TextBlock _udpRedirectPort; + private readonly Border _adminStatusCard; + private readonly StackPanel _ruleList; + private readonly List _rules = new(); + private readonly Button _saveRoutingButton; + private readonly Button _discardRoutingButton; + private bool _refreshing; + private bool _rulesDirty; + private bool _captureModeChanging; + + internal TrafficPage(WinUIPageContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + + Content = WinUIStyles.CreatePage( + "Traffic Routing", + "Choose User or Administrator capture, inspect WinDivert state, and route individual applications.", + out StackPanel panel); + + var captureCard = new StackPanel { Spacing = 12 }; + captureCard.Children.Add(WinUIStyles.CreateSectionTitle("Capture mode")); + _userMode = new RadioButton + { + Content = "User Mode", + GroupName = "CaptureMode", + }; + _userMode.Checked += OnCaptureModeChecked; + _context.SetToolTip(_userMode, "Use the normal user-mode local proxy path without elevation or WinDivert capture."); + captureCard.Children.Add(_userMode); + TextBlock userDescription = WinUIStyles.CreateText("No elevation. Uses the normal local proxy path.", "CaptionTextBlockStyle"); + userDescription.Opacity = 0.72; + captureCard.Children.Add(userDescription); + var adminModeContent = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 7, + }; + adminModeContent.Children.Add(new FontIcon + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uEA18", + FontSize = 16, + }); + adminModeContent.Children.Add(new TextBlock + { + Text = "Admin Mode", + VerticalAlignment = VerticalAlignment.Center, + }); + _adminMode = new RadioButton + { + Content = adminModeContent, + GroupName = "CaptureMode", + }; + _adminMode.Checked += OnCaptureModeChecked; + _context.SetToolTip(_adminMode, "Request elevation and enable transparent TCP/UDP capture through NetworkService and WinDivert."); + captureCard.Children.Add(_adminMode); + TextBlock adminDescription = WinUIStyles.CreateText("Transparent TCP/UDP capture through the elevated NetworkService helper.", "CaptionTextBlockStyle"); + adminDescription.Opacity = 0.72; + captureCard.Children.Add(adminDescription); + _captureModeProgress = new ProgressRing + { + Width = 20, + Height = 20, + IsActive = false, + Visibility = Visibility.Collapsed, + HorizontalAlignment = HorizontalAlignment.Left, + }; + _context.SetToolTip(_captureModeProgress, "Applying traffic capture mode…"); + captureCard.Children.Add(_captureModeProgress); + panel.Children.Add(WinUIStyles.CreateCard(captureCard)); + + var stateCard = new StackPanel { Spacing = 10 }; + stateCard.Children.Add(WinUIStyles.CreateSectionTitle("Administrator capture status")); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("Runtime mode", out _runtimeMode)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("WinDivert", out _winDivertStatus)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("Network service", out _networkServiceStatus)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("TCP capture", out _tcpCaptureStatus)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("UDP capture", out _udpCaptureStatus)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("TCP redirect port", out _tcpRedirectPort)); + stateCard.Children.Add(WinUIStyles.CreateKeyValueRow("UDP redirect port", out _udpRedirectPort)); + _adminStatusCard = WinUIStyles.CreateCard(stateCard); + panel.Children.Add(_adminStatusCard); + + var windowsProxyCard = new StackPanel { Spacing = 14 }; + windowsProxyCard.Children.Add(WinUIStyles.CreateSectionTitle("Windows proxy")); + _systemProxyToggle = new ToggleSwitch + { + Header = "Use Shadowsocks as the Windows system proxy", + OnContent = "On", + OffContent = "Off", + }; + _systemProxyToggle.Toggled += OnSystemProxyToggled; + _context.SetToolTip(_systemProxyToggle, "Configure Windows to use this Shadowsocks instance as the system proxy."); + windowsProxyCard.Children.Add(_systemProxyToggle); + _shareLanToggle = new ToggleSwitch + { + Header = "Allow other devices to connect", + OnContent = "Shared", + OffContent = "Local only", + }; + _shareLanToggle.Toggled += OnShareLanToggled; + _context.SetToolTip(_shareLanToggle, "Allow devices on the local network to connect to the local Shadowsocks proxy port."); + windowsProxyCard.Children.Add(_shareLanToggle); + _localPortBox = new NumberBox + { + Header = "Local SOCKS/HTTP port", + Minimum = 1, + Maximum = 65535, + SmallChange = 1, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + Width = 220, + HorizontalAlignment = HorizontalAlignment.Left, + }; + _context.SetToolTip(_localPortBox, "Local SOCKS/HTTP port used by applications and the Windows proxy configuration."); + windowsProxyCard.Children.Add(_localPortBox); + var applyPort = new Button { Content = "Apply port", HorizontalAlignment = HorizontalAlignment.Left }; + _context.SetToolTip(applyPort, "Apply the local proxy port immediately."); + applyPort.Click += OnApplyPortClicked; + windowsProxyCard.Children.Add(applyPort); + panel.Children.Add(WinUIStyles.CreateCard(windowsProxyCard)); + + var routesCard = new StackPanel { Spacing = 12 }; + routesCard.Children.Add(WinUIStyles.CreateSectionTitle("Application routing")); + routesCard.Children.Add(WinUIStyles.CreateText( + "Rules are matched by executable name, full path, or wildcard. Proxy, Direct and Block are explicit actions.")); + routesCard.Children.Add(CreateRoutingHeader()); + _ruleList = new StackPanel { Spacing = 8 }; + routesCard.Children.Add(_ruleList); + var ruleActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + var addRule = new Button { Content = "Add application" }; + _context.SetToolTip(addRule, "Add a per-application routing rule."); + addRule.Click += (_, _) => AddRule(new ApplicationRouteRule(), markDirty: true); + ruleActions.Children.Add(addRule); + _saveRoutingButton = new Button { Content = "Save routing", IsEnabled = false }; + _context.SetToolTip(_saveRoutingButton, "Validate and save all application routing rules."); + _saveRoutingButton.Click += OnSaveRoutingClicked; + ruleActions.Children.Add(_saveRoutingButton); + _discardRoutingButton = new Button { Content = "Discard changes", IsEnabled = false }; + _context.SetToolTip(_discardRoutingButton, "Discard unsaved routing-rule changes and reload the saved configuration."); + _discardRoutingButton.Click += (_, _) => + { + SetRulesDirty(false); + Refresh(); + }; + ruleActions.Children.Add(_discardRoutingButton); + routesCard.Children.Add(ruleActions); + panel.Children.Add(WinUIStyles.CreateCard(routesCard)); + + panel.Children.Add(new InfoBar + { + IsOpen = true, + IsClosable = false, + Severity = InfoBarSeverity.Informational, + Title = "Game Mode is automatic", + Message = "A configured game temporarily changes the runtime state to Game and suspends WinDivert. It is not a third selectable capture mode.", + }); + + WinUILocalization.Apply(Content, _context.Localization); + } + + public void Refresh() + { + Shadowsocks.Controller.ShadowsocksController? controller = _context.Controller; + Configuration? configuration = controller?.GetCurrentConfiguration(); + if (controller is null || configuration is null) + { + return; + } + + _refreshing = true; + try + { + _systemProxyToggle.IsOn = configuration.Enabled; + _systemProxyToggle.IsEnabled = configuration.HasConfiguredServer; + _shareLanToggle.IsOn = configuration.shareOverLan; + _localPortBox.Value = configuration.localPort; + + TrafficCaptureStatus status = controller.GetTrafficCaptureStatus(); + _runtimeMode.Text = _context.L(status.RuntimeMode.ToString()); + _winDivertStatus.Text = status.GameModeActive + ? _context.L("Suspended by Game Mode") + : status.WinDivertActive ? _context.L("Active") : _context.L("Inactive"); + _networkServiceStatus.Text = status.NetworkServiceRunning ? _context.L("Running") : _context.L("Stopped"); + _tcpCaptureStatus.Text = status.GameModeActive ? _context.L("Suspended") : status.TcpCaptureActive ? _context.L("Active") : _context.L("Inactive"); + _udpCaptureStatus.Text = status.GameModeActive ? _context.L("Suspended") : status.UdpCaptureActive ? _context.L("Active") : _context.L("Inactive"); + _tcpRedirectPort.Text = status.TcpRedirectPort > 0 ? status.TcpRedirectPort.ToString(System.Globalization.CultureInfo.InvariantCulture) : "—"; + _udpRedirectPort.Text = status.UdpRedirectPort > 0 ? status.UdpRedirectPort.ToString(System.Globalization.CultureInfo.InvariantCulture) : "—"; + bool showAdminStatus = configuration.trafficCaptureMode == TrafficCaptureMode.Admin + || status.RuntimeMode is TrafficRuntimeMode.Admin or TrafficRuntimeMode.Game; + _adminStatusCard.Visibility = showAdminStatus ? Visibility.Visible : Visibility.Collapsed; + + if (!_rulesDirty) + { + _userMode.IsChecked = configuration.trafficCaptureMode == TrafficCaptureMode.User; + _adminMode.IsChecked = configuration.trafficCaptureMode == TrafficCaptureMode.Admin; + LoadRules(configuration.applicationRules ?? []); + } + } + finally + { + _refreshing = false; + } + } + + + private static Grid CreateRoutingHeader() + { + var grid = new Grid + { + ColumnSpacing = 8, + Margin = new Thickness(0, 2, 0, 0), + }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(140) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + TextBlock enabled = WinUIStyles.CreateText("Enabled", "CaptionTextBlockStyle"); + enabled.MinWidth = 64; + grid.Children.Add(enabled); + + TextBlock application = WinUIStyles.CreateText("Application", "CaptionTextBlockStyle"); + Grid.SetColumn(application, 1); + grid.Children.Add(application); + + TextBlock action = WinUIStyles.CreateText("Action", "CaptionTextBlockStyle"); + Grid.SetColumn(action, 2); + grid.Children.Add(action); + + TextBlock operations = WinUIStyles.CreateText("", "CaptionTextBlockStyle"); + operations.MinWidth = 72; + Grid.SetColumn(operations, 3); + grid.Children.Add(operations); + return grid; + } + + private void LoadRules(IEnumerable rules) + { + _rules.Clear(); + _ruleList.Children.Clear(); + foreach (ApplicationRouteRule rule in rules.Where(rule => rule is not null)) + { + AddRule(rule, markDirty: false); + } + + if (_rules.Count == 0) + { + TextBlock empty = WinUIStyles.CreateText(_context.L("No application-specific rules.")); + empty.Opacity = 0.68; + _ruleList.Children.Add(empty); + } + SetRulesDirty(false); + } + + private void AddRule(ApplicationRouteRule rule, bool markDirty) + { + if (_rules.Count == 0) + { + _ruleList.Children.Clear(); + } + + var grid = new Grid { ColumnSpacing = 8 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(140) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var enabled = new CheckBox + { + IsChecked = rule.Enabled, + VerticalAlignment = VerticalAlignment.Center, + }; + enabled.Checked += (_, _) => MarkRulesDirty(); + enabled.Unchecked += (_, _) => MarkRulesDirty(); + _context.SetToolTip(enabled, "Enable or disable this application routing rule without deleting it."); + grid.Children.Add(enabled); + + var application = new TextBox + { + Text = rule.Application ?? string.Empty, + PlaceholderText = "application.exe", + }; + application.TextChanged += (_, _) => MarkRulesDirty(); + Grid.SetColumn(application, 1); + _context.SetToolTip(application, "Executable name, full path, or wildcard pattern used to match a process."); + grid.Children.Add(application); + + var action = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; + action.Items.Add(new ComboBoxItem { Content = _context.L("Proxy"), Tag = TrafficRouteAction.Proxy }); + action.Items.Add(new ComboBoxItem { Content = _context.L("Direct"), Tag = TrafficRouteAction.Direct }); + action.Items.Add(new ComboBoxItem { Content = _context.L("Block"), Tag = TrafficRouteAction.Block }); + TrafficRouteAction normalizedAction = rule.Action == TrafficRouteAction.Default ? TrafficRouteAction.Proxy : rule.Action; + foreach (object item in action.Items) + { + if (item is ComboBoxItem comboItem && comboItem.Tag is TrafficRouteAction itemAction && itemAction == normalizedAction) + { + action.SelectedItem = comboItem; + break; + } + } + action.SelectionChanged += (_, _) => MarkRulesDirty(); + _context.SetToolTip(action, "Choose whether matching traffic is proxied, sent directly, or blocked."); + Grid.SetColumn(action, 2); + grid.Children.Add(action); + + var remove = new Button { Content = _context.L("Remove") }; + _context.SetToolTip(remove, "Remove this application routing rule."); + Grid.SetColumn(remove, 3); + grid.Children.Add(remove); + + var editor = new RuleEditor + { + Root = grid, + Enabled = enabled, + Application = application, + Action = action, + }; + remove.Click += (_, _) => + { + _rules.Remove(editor); + _ruleList.Children.Remove(grid); + SetRulesDirty(true); + if (_rules.Count == 0) + { + TextBlock empty = WinUIStyles.CreateText(_context.L("No application-specific rules.")); + empty.Opacity = 0.68; + _ruleList.Children.Add(empty); + } + }; + + _rules.Add(editor); + _ruleList.Children.Add(grid); + if (markDirty) + { + SetRulesDirty(true); + } + } + + private void MarkRulesDirty() + { + if (!_refreshing) + { + SetRulesDirty(true); + } + } + + private void SetRulesDirty(bool dirty) + { + _rulesDirty = dirty; + _saveRoutingButton.IsEnabled = dirty; + _discardRoutingButton.IsEnabled = dirty; + } + + private async void OnCaptureModeChecked(object sender, RoutedEventArgs _) + { + if (_refreshing || _captureModeChanging || _context.Controller is null) + { + return; + } + + TrafficCaptureMode requestedMode = ReferenceEquals(sender, _adminMode) + ? TrafficCaptureMode.Admin + : TrafficCaptureMode.User; + if (_context.Controller.GetCurrentConfiguration().trafficCaptureMode == requestedMode) + { + return; + } + + _captureModeChanging = true; + _userMode.IsEnabled = false; + _adminMode.IsEnabled = false; + _captureModeProgress.Visibility = Visibility.Visible; + _captureModeProgress.IsActive = true; + try + { + bool applied = await _context.Controller.SetTrafficCaptureModeAsync(requestedMode); + if (!applied) + { + _context.ShowInfo( + "Traffic Routing", + requestedMode == TrafficCaptureMode.Admin + ? "Administrator mode could not be enabled." + : "User mode could not be enabled.", + InfoBarSeverity.Warning); + } + } + finally + { + _captureModeProgress.IsActive = false; + _captureModeProgress.Visibility = Visibility.Collapsed; + _captureModeChanging = false; + _userMode.IsEnabled = true; + _adminMode.IsEnabled = true; + Refresh(); + } + } + + private void OnSystemProxyToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + if (!_context.Controller.GetCurrentConfiguration().HasConfiguredServer) + { + _refreshing = true; + try + { + _systemProxyToggle.IsOn = false; + } + finally + { + _refreshing = false; + } + _context.ShowInfo( + "Windows proxy", + "Add a configured Shadowsocks server before enabling the Windows system proxy.", + InfoBarSeverity.Warning); + return; + } + + _context.Controller.ToggleEnable(_systemProxyToggle.IsOn); + } + + private void OnShareLanToggled(object _, RoutedEventArgs _1) + { + if (_refreshing || _context.Controller is null) + { + return; + } + + _context.Controller.ToggleShareOverLAN(_shareLanToggle.IsOn); + } + + private void OnApplyPortClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null || double.IsNaN(_localPortBox.Value)) + { + return; + } + + try + { + int port = checked((int)_localPortBox.Value); + _context.Controller.SetLocalPort(port); + if (_context.Controller.LastListenerError is null) + { + _context.ShowInfo("Local proxy", _context.LF("Local proxy port changed to {0}.", port), InfoBarSeverity.Success); + } + } + catch (Exception exception) + { + _context.ShowInfo("Invalid port", exception.Message, InfoBarSeverity.Error); + Refresh(); + } + } + + private async void OnSaveRoutingClicked(object _, RoutedEventArgs _1) + { + if (_context.Controller is null) + { + return; + } + + TrafficCaptureMode mode = _context.Controller.GetCurrentConfiguration().trafficCaptureMode; + var rules = new List(); + foreach (RuleEditor editor in _rules) + { + string application = editor.Application.Text?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(application)) + { + continue; + } + + TrafficRouteAction action = editor.Action.SelectedItem is ComboBoxItem { Tag: TrafficRouteAction selectedAction } + ? selectedAction + : TrafficRouteAction.Proxy; + if (action is TrafficRouteAction.Default) + { + action = TrafficRouteAction.Proxy; + } + rules.Add(new ApplicationRouteRule + { + Enabled = editor.Enabled.IsChecked == true, + Application = application, + Action = action, + }); + } + + _saveRoutingButton.IsEnabled = false; + try + { + Configuration configuration = _context.Controller.GetCurrentConfiguration(); + bool saved = await _context.Controller.SaveTrafficRoutingAsync( + mode, + rules, + configuration.gameModeApplications ?? []); + SetRulesDirty(!saved); + _context.ShowInfo( + "Traffic Routing", + saved ? "Traffic routing applied." : "The requested routing configuration could not be applied.", + saved ? InfoBarSeverity.Success : InfoBarSeverity.Warning); + } + finally + { + _saveRoutingButton.IsEnabled = _rulesDirty; + _discardRoutingButton.IsEnabled = _rulesDirty; + } + Refresh(); + } +} diff --git a/Shadowsocks.WinUI/Program.cs b/Shadowsocks.WinUI/Program.cs new file mode 100644 index 000000000..bb0eebbe9 --- /dev/null +++ b/Shadowsocks.WinUI/Program.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using Shadowsocks.Core.Storage; +using Shadowsocks.Core; +using AppRuntimeEnvironment = Shadowsocks.Core.RuntimeEnvironment; +using System.Linq; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Service; +using Shadowsocks.Localization; +using Windows.ApplicationModel.Activation; + +namespace Shadowsocks.WinUI; + +internal static partial class Program +{ + private const uint Infinite = 0xFFFFFFFF; + private static readonly ConcurrentQueue PendingActivations = new(); + private static Action? _activationHandler; + private static ILocalizationService? _localization; + private static ProcessSingleInstanceGuard? _processInstanceGuard; + private static App? _application; + + [STAThread] + public static int Main() + { + string[] rawArguments = Environment.GetCommandLineArgs().Skip(1).ToArray(); + if (SelfUpdater.TryRunUpdaterMode(rawArguments, out int updaterExitCode)) + { + return updaterExitCode; + } + + SelfUpdater.CleanupCompletedUpdate(rawArguments); + string[] applicationArguments = SelfUpdater.RemoveInternalArguments(rawArguments); + InitializeProcessEnvironment(applicationArguments); + _localization = CsvLocalizationService.CreateDefault(); + I18N.Configure(_localization); + + AppInstance currentInstance = AppInstance.GetCurrent(); + AppActivationArguments? activationArguments = currentInstance.GetActivatedEventArgs(); + if (activationArguments is null) + { + return 1; + } + + AppInstance keyInstance = AppInstance.FindOrRegisterForKey(CreateInstanceKey()); + + if (!keyInstance.IsCurrent) + { + // Never create a second-process Win32 dialog. Forward every activation to + // the running WinUI instance so all user-facing UI stays Fluent/WinUI 3. + return RedirectActivation(activationArguments, keyInstance); + } + + // AppInstance registration for an unpackaged application can be scoped by the + // executable identity. The stable LocalAppData startup copy and the original + // product EXE therefore need an identity-independent process gate as well. + _processInstanceGuard = ProcessSingleInstanceGuard.TryAcquire(); + if (_processInstanceGuard is null) + { + Debug.WriteLine("Another Shadowsocks Reborn process already owns the process-wide single-instance gate."); + return 0; + } + + keyInstance.Activated += OnInstanceActivated; + App.ConfigureStartupContext(keyInstance, activationArguments, _localization); + + // DISABLE_XAML_GENERATED_MAIN gives us control of startup so AppInstance + // redirection can happen before WinUI is initialized. Start WinUI through + // the public application bootstrap rather than calling generated XAML APIs. + WinRT.ComWrappersSupport.InitializeComWrappers(); + try + { + Application.Start(static _ => + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _application = new App(); + }); + return 0; + } + finally + { + _application?.Dispose(); + _application = null; + _processInstanceGuard?.Dispose(); + _processInstanceGuard = null; + } + } + + private static void InitializeProcessEnvironment(string[] commandLineArguments) + { + string executablePath = Environment.ProcessPath + ?? Process.GetCurrentProcess().MainModule?.FileName + ?? Path.Combine(AppContext.BaseDirectory, "Shadowsocks.exe"); + string workingDirectory = Path.GetDirectoryName(executablePath) ?? AppContext.BaseDirectory; + + AppRuntimeEnvironment.Initialize(executablePath, workingDirectory, commandLineArguments); + AppStoragePaths.Initialize(executablePath); + Directory.SetCurrentDirectory(workingDirectory); + } + + internal static void RegisterActivationHandler(Action handler) + { + ArgumentNullException.ThrowIfNull(handler); + _activationHandler = handler; + + while (PendingActivations.TryDequeue(out AppActivationArguments? activation)) + { + if (activation is not null) + { + handler(activation); + } + } + } + + internal static void UnregisterActivationHandler(Action handler) + { + if (_activationHandler == handler) + { + _activationHandler = null; + } + } + + internal static void DetachActivationSource(AppInstance appInstance) + { + ArgumentNullException.ThrowIfNull(appInstance); + appInstance.Activated -= OnInstanceActivated; + } + + private static void OnInstanceActivated(object? _, AppActivationArguments activationArguments) + { + Action? handler = _activationHandler; + if (handler is null) + { + PendingActivations.Enqueue(activationArguments); + return; + } + + handler(activationArguments); + } + + + internal static bool IsPlainLaunch(AppActivationArguments activationArguments) + { + if (activationArguments.Kind != ExtendedActivationKind.Launch) + { + return false; + } + + if (activationArguments.Data is not ILaunchActivatedEventArgs launchArguments) + { + return true; + } + + string arguments = launchArguments.Arguments?.Trim() ?? string.Empty; + return !arguments.Contains("--open-url", StringComparison.OrdinalIgnoreCase) + && !arguments.Contains(AutoStartup.StartupHiddenOption, StringComparison.OrdinalIgnoreCase) + && !arguments.Contains(AutoStartup.StartupVisibleOption, StringComparison.OrdinalIgnoreCase) + && !arguments.Contains(AutoStartup.StartupOriginOption, StringComparison.OrdinalIgnoreCase); + } + + private static string CreateInstanceKey() + { + // One instance per user prevents normal and Clean Mode processes from racing on + // system proxy state, hotkeys, tray ownership, and Admin Mode resources. + return "Shadowsocks.Reborn.WinUI"; + } + + private static int RedirectActivation(AppActivationArguments activationArguments, AppInstance keyInstance) + { + nint redirectEvent = CreateEvent(nint.Zero, 1, 0, null); + if (redirectEvent == nint.Zero) + { + return 2; + } + + Exception? redirectException = null; + _ = Task.Run(async () => + { + try + { + await keyInstance.RedirectActivationToAsync(activationArguments).AsTask().ConfigureAwait(false); + } + catch (Exception exception) + { + redirectException = exception; + } + finally + { + _ = SetEvent(redirectEvent); + } + }); + + try + { + _ = WaitForRedirectCompletion(redirectEvent); + + if (redirectException is not null) + { + Debug.WriteLine(redirectException); + return 3; + } + + try + { + using Process process = Process.GetProcessById(unchecked((int)keyInstance.ProcessId)); + if (process.MainWindowHandle != nint.Zero) + { + _ = SetForegroundWindow(process.MainWindowHandle); + } + } + catch (ArgumentException) + { + // The target instance handles the redirected activation itself. + } + + return 0; + } + finally + { + _ = CloseHandle(redirectEvent); + } + } + + private static unsafe uint WaitForRedirectCompletion(nint redirectEvent) + { + nint* handles = stackalloc nint[1]; + handles[0] = redirectEvent; + return CoWaitForMultipleObjects(0, Infinite, 1, handles, out _); + } + + [LibraryImport("kernel32.dll", EntryPoint = "CreateEventW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + private static partial nint CreateEvent(nint eventAttributes, int manualReset, int initialState, string? name); + + [LibraryImport("kernel32.dll", EntryPoint = "SetEvent", SetLastError = true)] + private static partial int SetEvent(nint eventHandle); + + [LibraryImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true)] + private static partial int CloseHandle(nint handle); + + [LibraryImport("ole32.dll", EntryPoint = "CoWaitForMultipleObjects")] + private static unsafe partial uint CoWaitForMultipleObjects( + uint flags, + uint timeoutMilliseconds, + uint handleCount, + nint* handles, + out uint index); + + [LibraryImport("user32.dll", EntryPoint = "SetForegroundWindow")] + private static partial int SetForegroundWindow(nint windowHandle); +} diff --git a/Shadowsocks.WinUI/Properties/PublishProfiles/FolderProfile.pubxml b/Shadowsocks.WinUI/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 000000000..c3be99db0 --- /dev/null +++ b/Shadowsocks.WinUI/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,22 @@ + + + + Release + net10.0-windows10.0.26100.0 + x64 + win-x64 + None + true + true + true + true + true + true + true + false + false + true + None + false + + diff --git a/Shadowsocks.WinUI/Properties/launchSettings.json b/Shadowsocks.WinUI/Properties/launchSettings.json new file mode 100644 index 000000000..eed90fa19 --- /dev/null +++ b/Shadowsocks.WinUI/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Shadowsocks.WinUI (Unpackaged)": { + "commandName": "Project" + } + } +} diff --git a/Shadowsocks.WinUI/Shadowsocks.WinUI.csproj b/Shadowsocks.WinUI/Shadowsocks.WinUI.csproj new file mode 100644 index 000000000..fcea0159e --- /dev/null +++ b/Shadowsocks.WinUI/Shadowsocks.WinUI.csproj @@ -0,0 +1,243 @@ + + + WinExe + net10.0-windows10.0.26100.0 + 10.0.19041.0 + 10.0.19041.0 + Shadowsocks community + https://github.com/SlimRG/shadowsocks-reborn + git + Shadowsocks.WinUI + Shadowsocks + shadowsocks.ico + app.manifest + true + + true + None + true + true + x64 + x64 + win-x64 + disable + enable + true + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN + false + false + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Core\Shadowsocks.Core.csproj')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows\Shadowsocks.Windows.csproj')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows.WinUI\Shadowsocks.Windows.WinUI.csproj')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Core\obj\project.assets.json')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows\obj\project.assets.json')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows.WinUI\obj\project.assets.json')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Core\bin\x64\$(Configuration)\net10.0\Shadowsocks.Core.dll')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows\bin\x64\$(Configuration)\net10.0-windows10.0.19041.0\Shadowsocks.Windows.dll')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.Windows.WinUI\bin\x64\$(Configuration)\net10.0-windows10.0.26100.0\Shadowsocks.Windows.WinUI.dll')) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.NetworkService\Shadowsocks.NetworkService.csproj')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.NetworkService\bin\x64\$(Configuration)\net10.0-windows10.0.19041.0\win-x64\')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\Shadowsocks.NetworkService\bin\ProductPublish\$(Configuration)\win-x64\')) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Shadowsocks.WinUI/Shell/WinUIHotkeyManager.cs b/Shadowsocks.WinUI/Shell/WinUIHotkeyManager.cs new file mode 100644 index 000000000..92a581cc2 --- /dev/null +++ b/Shadowsocks.WinUI/Shell/WinUIHotkeyManager.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.UI.Dispatching; +using NLog; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Hotkeys; +using Shadowsocks.Model; + +namespace Shadowsocks.WinUI.Shell; + +internal sealed class WinUIHotkeyManager : IDisposable +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly ShadowsocksController _controller; + private readonly DispatcherQueue _dispatcherQueue; + private readonly Action _showWindow; + private readonly Action _showLogsPage; + private readonly GlobalHotkeyService _hotkeys = new(); + private bool _disposed; + + public WinUIHotkeyManager( + ShadowsocksController controller, + DispatcherQueue dispatcherQueue, + Action showWindow, + Action showLogsPage) + { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(dispatcherQueue); + ArgumentNullException.ThrowIfNull(showWindow); + ArgumentNullException.ThrowIfNull(showLogsPage); + _controller = controller; + _dispatcherQueue = dispatcherQueue; + _showWindow = showWindow; + _showLogsPage = showLogsPage; + } + + public IReadOnlyList RegisterConfiguredHotkeys() + { + HotkeyConfig? config = _controller.GetCurrentConfiguration().hotkey; + return ApplyConfiguration(config, respectStartupFlag: true); + } + + public IReadOnlyList ApplyConfiguration(HotkeyConfig? config, bool respectStartupFlag = false) + { + ObjectDisposedException.ThrowIf(_disposed, this); + UnregisterAll(); + + if (config is null || (respectStartupFlag && !config.RegHotkeysAtStartup)) + { + return Array.Empty(); + } + + var failures = new List(); + Register("SwitchSystemProxy", config.SwitchSystemProxy, ToggleSystemProxy, failures); + Register("SwitchSystemProxyMode", config.SwitchSystemProxyMode, ToggleSystemProxyMode, failures); + Register("SwitchAllowLan", config.SwitchAllowLan, ToggleAllowLan, failures); + Register("ShowLogs", config.ShowLogs, ShowLogs, failures); + Register("ServerMoveUp", config.ServerMoveUp, () => MoveServer(-1), failures); + Register("ServerMoveDown", config.ServerMoveDown, () => MoveServer(1), failures); + return failures; + } + + private void UnregisterAll() + { + _hotkeys.Unregister("SwitchSystemProxy"); + _hotkeys.Unregister("SwitchSystemProxyMode"); + _hotkeys.Unregister("SwitchAllowLan"); + _hotkeys.Unregister("ShowLogs"); + _hotkeys.Unregister("ServerMoveUp"); + _hotkeys.Unregister("ServerMoveDown"); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _hotkeys.Dispose(); + } + + private void Register(string name, string gesture, Action callback, List failures) + { + if (string.IsNullOrWhiteSpace(gesture)) + { + _hotkeys.Unregister(name); + return; + } + + if (!HotkeyGesture.TryParse(gesture, out _)) + { + Logger.Warn("Cannot parse configured hotkey {0}: {1}", name, gesture); + failures.Add(name); + return; + } + + bool registered = _hotkeys.Register(name, gesture, () => _dispatcherQueue.TryEnqueue(() => callback())); + if (!registered) + { + Logger.Warn("Cannot register configured hotkey {0}: {1}", name, gesture); + failures.Add(name); + } + } + + private void ToggleSystemProxy() + { + Configuration config = _controller.GetCurrentConfiguration(); + _controller.ToggleEnable(!config.Enabled); + } + + private void ToggleSystemProxyMode() + { + Configuration config = _controller.GetCurrentConfiguration(); + if (config.Enabled) + { + _controller.ToggleGlobal(!config.global); + } + } + + private void ToggleAllowLan() + { + Configuration config = _controller.GetCurrentConfiguration(); + _controller.ToggleShareOverLAN(!config.shareOverLan); + } + + private void ShowLogs() + { + _showLogsPage(); + } + + private void MoveServer(int delta) + { + Configuration config = _controller.GetCurrentConfiguration(); + int[] configuredIndices = (config.configs ?? []) + .Select((server, index) => (server, index)) + .Where(item => item.server?.IsConfigured == true) + .Select(item => item.index) + .ToArray(); + if (configuredIndices.Length == 0) + { + return; + } + + int position = Array.IndexOf(configuredIndices, config.index); + if (position < 0) + { + position = delta >= 0 ? -1 : 0; + } + + position = (position + delta + configuredIndices.Length) % configuredIndices.Length; + _controller.SelectServerIndex(configuredIndices[position]); + } +} diff --git a/Shadowsocks.WinUI/UI/AppThemePreference.cs b/Shadowsocks.WinUI/UI/AppThemePreference.cs new file mode 100644 index 000000000..3efed9981 --- /dev/null +++ b/Shadowsocks.WinUI/UI/AppThemePreference.cs @@ -0,0 +1,8 @@ +namespace Shadowsocks.WinUI.UI; + +internal enum AppThemePreference +{ + System, + Light, + Dark, +} diff --git a/Shadowsocks.WinUI/UI/IRefreshablePage.cs b/Shadowsocks.WinUI/UI/IRefreshablePage.cs new file mode 100644 index 000000000..85b665a9e --- /dev/null +++ b/Shadowsocks.WinUI/UI/IRefreshablePage.cs @@ -0,0 +1,6 @@ +namespace Shadowsocks.WinUI.UI; + +internal interface IRefreshablePage +{ + void Refresh(); +} diff --git a/Shadowsocks.WinUI/UI/WinUILocalization.cs b/Shadowsocks.WinUI/UI/WinUILocalization.cs new file mode 100644 index 000000000..ee268fc93 --- /dev/null +++ b/Shadowsocks.WinUI/UI/WinUILocalization.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Localization; + +namespace Shadowsocks.WinUI.UI; + +internal static class WinUILocalization +{ + public static void Apply(DependencyObject? root, ILocalizationService localization) + { + if (root is null) + { + return; + } + + ArgumentNullException.ThrowIfNull(localization); + LocalizeObject(root, localization); + VisitChildren(root, localization); + } + + private static void LocalizeObject(DependencyObject element, ILocalizationService localization) + { + switch (element) + { + case TextBlock textBlock when !string.IsNullOrWhiteSpace(textBlock.Text): + textBlock.Text = localization[textBlock.Text]; + break; + + case ToggleSwitch toggleSwitch: + toggleSwitch.Header = LocalizeObjectValue(toggleSwitch.Header, localization); + toggleSwitch.OnContent = LocalizeObjectValue(toggleSwitch.OnContent, localization); + toggleSwitch.OffContent = LocalizeObjectValue(toggleSwitch.OffContent, localization); + break; + + case TextBox textBox: + textBox.Header = LocalizeObjectValue(textBox.Header, localization); + if (!string.IsNullOrWhiteSpace(textBox.PlaceholderText)) + { + textBox.PlaceholderText = localization[textBox.PlaceholderText]; + } + break; + + case PasswordBox passwordBox: + passwordBox.Header = LocalizeObjectValue(passwordBox.Header, localization); + if (!string.IsNullOrWhiteSpace(passwordBox.PlaceholderText)) + { + passwordBox.PlaceholderText = localization[passwordBox.PlaceholderText]; + } + break; + + case NumberBox numberBox: + numberBox.Header = LocalizeObjectValue(numberBox.Header, localization); + if (!string.IsNullOrWhiteSpace(numberBox.PlaceholderText)) + { + numberBox.PlaceholderText = localization[numberBox.PlaceholderText]; + } + break; + + case ComboBox comboBox: + comboBox.Header = LocalizeObjectValue(comboBox.Header, localization); + if (!string.IsNullOrWhiteSpace(comboBox.PlaceholderText)) + { + comboBox.PlaceholderText = localization[comboBox.PlaceholderText]; + } + break; + + case InfoBar infoBar: + if (!string.IsNullOrWhiteSpace(infoBar.Title)) + { + infoBar.Title = localization[infoBar.Title]; + } + if (!string.IsNullOrWhiteSpace(infoBar.Message)) + { + infoBar.Message = localization[infoBar.Message]; + } + break; + + case TeachingTip teachingTip: + if (!string.IsNullOrWhiteSpace(teachingTip.Title)) + { + teachingTip.Title = localization[teachingTip.Title]; + } + if (!string.IsNullOrWhiteSpace(teachingTip.Subtitle)) + { + teachingTip.Subtitle = localization[teachingTip.Subtitle]; + } + break; + + case ContentControl contentControl: + contentControl.Content = LocalizeObjectValue(contentControl.Content, localization); + break; + } + } + + private static object? LocalizeObjectValue(object? value, ILocalizationService localization) + => value is string text && !string.IsNullOrWhiteSpace(text) ? localization[text] : value; + + private static void VisitChildren(DependencyObject element, ILocalizationService localization) + { + switch (element) + { + case Panel panel: + foreach (UIElement child in panel.Children) + { + Apply(child, localization); + } + break; + + case Border border: + Apply(border.Child, localization); + break; + + case ScrollViewer scrollViewer when scrollViewer.Content is DependencyObject scrollContent: + Apply(scrollContent, localization); + break; + + case ContentControl contentControl when contentControl.Content is DependencyObject content: + Apply(content, localization); + break; + + case ItemsControl itemsControl: + foreach (object item in itemsControl.Items) + { + if (item is DependencyObject dependencyObject) + { + Apply(dependencyObject, localization); + } + } + break; + } + } +} diff --git a/Shadowsocks.WinUI/UI/WinUIPageContext.cs b/Shadowsocks.WinUI/UI/WinUIPageContext.cs new file mode 100644 index 000000000..d5a154cec --- /dev/null +++ b/Shadowsocks.WinUI/UI/WinUIPageContext.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Shadowsocks.Controller; +using Shadowsocks.Model; +using Shadowsocks.Localization; + +namespace Shadowsocks.WinUI.UI; + +internal sealed class WinUIPageContext +{ + public WinUIPageContext( + ShadowsocksController? controller, + Func getXamlRoot, + Func getWindowHandle, + Action navigateToSharingServer, + Action showInfo, + Func> showDialogAsync, + Func getTheme, + Action setTheme, + Func getStartWithWindows, + Func> setStartWithWindows, + Action setAlwaysOnTop, + Func> registerHotkeys, + Func> applyHotkeys, + Action requestApplicationExit, + ILocalizationService localization) + { + Controller = controller; + GetXamlRoot = getXamlRoot; + GetWindowHandle = getWindowHandle; + NavigateToSharingServer = navigateToSharingServer; + _showInfo = showInfo; + _showDialogAsync = showDialogAsync; + GetTheme = getTheme; + SetTheme = setTheme; + GetStartWithWindows = getStartWithWindows; + SetStartWithWindows = setStartWithWindows; + SetAlwaysOnTop = setAlwaysOnTop; + RegisterHotkeys = registerHotkeys; + ApplyHotkeys = applyHotkeys; + ArgumentNullException.ThrowIfNull(requestApplicationExit); + ArgumentNullException.ThrowIfNull(localization); + RequestApplicationExit = requestApplicationExit; + Localization = localization; + } + + public ShadowsocksController? Controller { get; } + public Func GetXamlRoot { get; } + public Func GetWindowHandle { get; } + public Action NavigateToSharingServer { get; } + private readonly Action _showInfo; + private readonly Func> _showDialogAsync; + + public void ShowInfo(string title, string message, InfoBarSeverity severity) + => _showInfo(Localization[title], Localization[message], severity); + + public Task ShowDialogAsync(string title, string message, string closeButtonText) + => _showDialogAsync(Localization[title], Localization[message], Localization[closeButtonText]); + public Func GetTheme { get; } + public Action SetTheme { get; } + public Func GetStartWithWindows { get; } + public Func> SetStartWithWindows { get; } + public Action SetAlwaysOnTop { get; } + public Func> RegisterHotkeys { get; } + public Func> ApplyHotkeys { get; } + public Action RequestApplicationExit { get; } + public ILocalizationService Localization { get; } + + public string L(string key) => Localization[key]; + public string LF(string key, params object[] args) => Localization.Format(key, args); + + public void SetToolTip(FrameworkElement element, string key) + { + ArgumentNullException.ThrowIfNull(element); + string text = L(key); + ToolTipService.SetToolTip(element, text); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetHelpText(element, text); + } +} diff --git a/Shadowsocks.WinUI/UI/WinUIStyles.cs b/Shadowsocks.WinUI/UI/WinUIStyles.cs new file mode 100644 index 000000000..49fe103cc --- /dev/null +++ b/Shadowsocks.WinUI/UI/WinUIStyles.cs @@ -0,0 +1,125 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace Shadowsocks.WinUI.UI; + +internal static class WinUIStyles +{ + public const double PageGutter = 16; + public const double PageTopGutter = 16; + public const double CardPadding = 16; + + public static ScrollViewer CreatePage(string title, string subtitle, out StackPanel panel) + { + panel = new StackPanel + { + Spacing = 20, + Margin = new Thickness(0, PageTopGutter, 0, 0), + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + + var header = new StackPanel { Spacing = 4 }; + header.Children.Add(CreateText(title, "TitleTextBlockStyle")); + var subtitleBlock = CreateText(subtitle, "BodyTextBlockStyle"); + subtitleBlock.Opacity = 0.72; + subtitleBlock.TextWrapping = TextWrapping.Wrap; + header.Children.Add(subtitleBlock); + panel.Children.Add(header); + + return new ScrollViewer + { + Content = panel, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + }; + } + + public static Border CreateCard(UIElement content) + { + return new Border + { + Background = GetBrush("CardBackgroundFillColorDefaultBrush"), + BorderBrush = GetBrush("CardStrokeColorDefaultBrush"), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(8), + Padding = new Thickness(CardPadding), + HorizontalAlignment = HorizontalAlignment.Stretch, + Child = content, + }; + } + + public static TextBlock CreateText(string text, string styleKey = "BodyTextBlockStyle") + { + var textBlock = new TextBlock + { + Text = text, + TextWrapping = TextWrapping.Wrap, + }; + ApplyStyle(textBlock, styleKey); + return textBlock; + } + + public static TextBlock CreateSectionTitle(string text) + { + return CreateText(text, "BodyStrongTextBlockStyle"); + } + + public static FontIcon CreateWindows10Icon(string glyph, double fontSize = 16) + { + return new FontIcon + { + FontFamily = new FontFamily("Segoe MDL2 Assets"), + Glyph = glyph, + FontSize = fontSize, + }; + } + + + public static Grid CreateKeyValueRow(string label, out TextBlock value) + { + var grid = new Grid { ColumnSpacing = 24 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + + var labelBlock = CreateText(label, "BodyStrongTextBlockStyle"); + labelBlock.VerticalAlignment = VerticalAlignment.Top; + grid.Children.Add(labelBlock); + + value = CreateText("—"); + value.VerticalAlignment = VerticalAlignment.Top; + Grid.SetColumn(value, 1); + grid.Children.Add(value); + return grid; + } + + public static Brush? GetBrush(string key) + { + try + { + return Application.Current.Resources[key] as Brush; + } + catch (Exception) + { + return null; + } + } + + private static void ApplyStyle(FrameworkElement element, string key) + { + try + { + if (Application.Current.Resources[key] is Style style) + { + element.Style = style; + } + } + catch (Exception) + { + // The built-in type ramp is preferred, but the controls retain accessible defaults + // if a resource is unavailable on an older/fallback Windows environment. + } + } +} diff --git a/Shadowsocks.WinUI/app.manifest b/Shadowsocks.WinUI/app.manifest new file mode 100644 index 000000000..2bd22a75b --- /dev/null +++ b/Shadowsocks.WinUI/app.manifest @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + PerMonitorV2 + + + diff --git a/shadowsocks-csharp/shadowsocks.ico b/Shadowsocks.WinUI/shadowsocks.ico old mode 100755 new mode 100644 similarity index 100% rename from shadowsocks-csharp/shadowsocks.ico rename to Shadowsocks.WinUI/shadowsocks.ico diff --git a/Shadowsocks.Windows.WinUI/Shadowsocks.Windows.WinUI.csproj b/Shadowsocks.Windows.WinUI/Shadowsocks.Windows.WinUI.csproj new file mode 100644 index 000000000..33b1446b5 --- /dev/null +++ b/Shadowsocks.Windows.WinUI/Shadowsocks.Windows.WinUI.csproj @@ -0,0 +1,27 @@ + + + net10.0-windows10.0.26100.0 + 10.0.19041.0 + 10.0.19041.0 + Shadowsocks.Windows.WinUI + Shadowsocks.Windows.WinUI + x64 + x64 + true + disable + enable + + + + + + + + + + + + + + + diff --git a/Shadowsocks.Windows.WinUI/Shell/PowerModeMonitor.cs b/Shadowsocks.Windows.WinUI/Shell/PowerModeMonitor.cs new file mode 100644 index 000000000..f6964c98b --- /dev/null +++ b/Shadowsocks.Windows.WinUI/Shell/PowerModeMonitor.cs @@ -0,0 +1,89 @@ +#nullable enable + +using System; +using Microsoft.UI.Xaml; +using WinUIEx; +using WinUIEx.Messaging; + +namespace Shadowsocks.Windows.WinUI.Shell; + +/// +/// Receives Windows session-ending and power-broadcast messages from the WinUI +/// window without introducing Microsoft.Win32.SystemEvents, WinForms or WPF +/// dependencies. The window may stay hidden in the notification area; its HWND +/// still receives WM_QUERYENDSESSION, WM_ENDSESSION and WM_POWERBROADCAST. +/// +public sealed class PowerModeMonitor : IDisposable +{ + private const uint WmQueryEndSession = 0x0011; + private const uint WmEndSession = 0x0016; + private const uint WmPowerBroadcast = 0x0218; + private const long PbtApmSuspend = 0x0004; + private const long PbtApmResumeSuspend = 0x0007; + private const long PbtApmResumeAutomatic = 0x0012; + + private readonly WindowMessageMonitor _monitor; + private bool _disposed; + + public PowerModeMonitor(Window window) + { + ArgumentNullException.ThrowIfNull(window); + _monitor = new WindowMessageMonitor(window.GetWindowHandle()); + _monitor.WindowMessageReceived += OnWindowMessageReceived; + } + + public event EventHandler? SessionEnding; + public event EventHandler? SessionEndCancelled; + public event EventHandler? Suspending; + public event EventHandler? Resumed; + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _monitor.WindowMessageReceived -= OnWindowMessageReceived; + _monitor.Dispose(); + } + + private void OnWindowMessageReceived(object? sender, WindowMessageEventArgs e) + { + if (e.Message.MessageId == WmQueryEndSession) + { + // Capture the shell state before AppWindow.Closing can be raised as part + // of logoff/shutdown. A system close must not be mistaken for the user + // explicitly sending the application to the notification area. + SessionEnding?.Invoke(this, EventArgs.Empty); + return; + } + + if (e.Message.MessageId == WmEndSession) + { + // wParam == FALSE means Windows cancelled the logoff/shutdown after the + // earlier WM_QUERYENDSESSION notification. Resume normal close-to-tray behavior. + if (unchecked((long)e.Message.WParam) == 0) + { + SessionEndCancelled?.Invoke(this, EventArgs.Empty); + } + return; + } + + if (e.Message.MessageId != WmPowerBroadcast) + { + return; + } + + long powerEvent = unchecked((long)e.Message.WParam); + if (powerEvent == PbtApmSuspend) + { + Suspending?.Invoke(this, EventArgs.Empty); + } + else if (powerEvent == PbtApmResumeSuspend || powerEvent == PbtApmResumeAutomatic) + { + Resumed?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/Shadowsocks.Windows.WinUI/Shell/ScreenQrCodeScanner.cs b/Shadowsocks.Windows.WinUI/Shell/ScreenQrCodeScanner.cs new file mode 100644 index 000000000..a9a02d566 --- /dev/null +++ b/Shadowsocks.Windows.WinUI/Shell/ScreenQrCodeScanner.cs @@ -0,0 +1,146 @@ +#nullable enable + +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; +using ZXing; +using ZXing.Common; +using ZXing.QrCode; + +namespace Shadowsocks.Windows.WinUI.Shell; + +/// +/// Windows-only QR scanner used by the WinUI sharing page. Supports virtual-desktop capture, +/// image streams/files, and clipboard bitmaps without taking a dependency on Windows Forms. +/// +public static class ScreenQrCodeScanner +{ + private const int SmXVirtualScreen = 76; + private const int SmYVirtualScreen = 77; + private const int SmCxVirtualScreen = 78; + private const int SmCyVirtualScreen = 79; + + public static string? ScanFromScreen() + { + int x = GetSystemMetrics(SmXVirtualScreen); + int y = GetSystemMetrics(SmYVirtualScreen); + int width = GetSystemMetrics(SmCxVirtualScreen); + int height = GetSystemMetrics(SmCyVirtualScreen); + if (width <= 0 || height <= 0) + { + return null; + } + + using var fullImage = new Bitmap(width, height); + using (Graphics graphics = Graphics.FromImage(fullImage)) + { + graphics.CopyFromScreen(x, y, 0, 0, fullImage.Size, CopyPixelOperation.SourceCopy); + } + + return ScanBitmap(fullImage); + } + + public static string? ScanFromFile(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + using var source = new Bitmap(path); + return ScanBitmap(source); + } + + public static string? ScanFromStream(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + using Image image = Image.FromStream(stream, useEmbeddedColorManagement: false, validateImageData: true); + using var bitmap = new Bitmap(image); + return ScanBitmap(bitmap); + } + + public static async Task<(bool HasImage, string? Result)> ScanFromClipboardAsync() + { + DataPackageView content = Clipboard.GetContent(); + if (!content.Contains(StandardDataFormats.Bitmap)) + { + return (false, null); + } + + var reference = await content.GetBitmapAsync(); + using var randomStream = await reference.OpenReadAsync(); + using Stream stream = randomStream.AsStreamForRead(); + return (true, ScanFromStream(stream)); + } + + private static string? ScanBitmap(Bitmap fullImage) + { + const int maxTry = 10; + for (int i = 0; i < maxTry; i++) + { + int marginLeft = (int)((double)fullImage.Width * i / 2.5 / maxTry); + int marginTop = (int)((double)fullImage.Height * i / 2.5 / maxTry); + int cropWidth = fullImage.Width - marginLeft * 2; + int cropHeight = fullImage.Height - marginTop * 2; + if (cropWidth <= 0 || cropHeight <= 0) + { + break; + } + + var cropRect = new Rectangle(marginLeft, marginTop, cropWidth, cropHeight); + using var target = new Bitmap(fullImage.Width, fullImage.Height); + using (Graphics graphics = Graphics.FromImage(target)) + { + graphics.DrawImage(fullImage, new Rectangle(0, 0, target.Width, target.Height), cropRect, GraphicsUnit.Pixel); + } + + RGBLuminanceSource luminanceSource = CreateLuminanceSource(target); + var bitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource)); + Result? result = new QRCodeReader().decode(bitmap); + if (!string.IsNullOrWhiteSpace(result?.Text)) + { + return result.Text; + } + } + + return null; + } + + private static RGBLuminanceSource CreateLuminanceSource(Bitmap bitmap) + { + var rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + BitmapData? data = null; + try + { + data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + int stride = data.Stride; + int absoluteStride = Math.Abs(stride); + byte[] source = new byte[absoluteStride * bitmap.Height]; + Marshal.Copy(data.Scan0, source, 0, source.Length); + + int packedStride = checked(bitmap.Width * 4); + byte[] pixels = new byte[checked(packedStride * bitmap.Height)]; + for (int row = 0; row < bitmap.Height; row++) + { + int sourceRow = stride >= 0 ? row : bitmap.Height - 1 - row; + Buffer.BlockCopy(source, sourceRow * absoluteStride, pixels, row * packedStride, packedStride); + } + + return new RGBLuminanceSource( + pixels, + bitmap.Width, + bitmap.Height, + RGBLuminanceSource.BitmapFormat.BGRA32); + } + finally + { + if (data is not null) + { + bitmap.UnlockBits(data); + } + } + } + + [DllImport("user32.dll")] + private static extern int GetSystemMetrics(int index); +} diff --git a/shadowsocks-csharp/Resources/ss32Fill.png b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Fill.png similarity index 100% rename from shadowsocks-csharp/Resources/ss32Fill.png rename to Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Fill.png diff --git a/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32In.png b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32In.png new file mode 100644 index 000000000..d42220bac Binary files /dev/null and b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32In.png differ diff --git a/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Out.png b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Out.png new file mode 100644 index 000000000..3aad9a616 Binary files /dev/null and b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Out.png differ diff --git a/shadowsocks-csharp/Resources/ss32Outline.png b/Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Outline.png similarity index 100% rename from shadowsocks-csharp/Resources/ss32Outline.png rename to Shadowsocks.Windows.WinUI/Shell/TrayAssets/ss32Outline.png diff --git a/Shadowsocks.Windows.WinUI/Shell/TrayIconService.cs b/Shadowsocks.Windows.WinUI/Shell/TrayIconService.cs new file mode 100644 index 000000000..6aaa1d677 --- /dev/null +++ b/Shadowsocks.Windows.WinUI/Shell/TrayIconService.cs @@ -0,0 +1,590 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.UI; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using NLog; +using WinUIEx; + +namespace Shadowsocks.Windows.WinUI.Shell; + +public enum TraySystemProxyMode +{ + Disabled, + Pac, + Global, +} + +public enum TrayTrafficMode +{ + User, + Admin, +} + +public enum TrayDnsMode +{ + System, + Direct, + Proxy, + CustomDoh, + DnsCrypt, +} + +public enum TrayCommandKind +{ + OpenOverview, + OpenServers, + OpenPlugins, + OpenTraffic, + OpenDns, + OpenSharing, + OpenPac, + OpenForwardProxy, + OpenOnlineConfig, + OpenHotkeys, + OpenLogs, + OpenAbout, + SetSystemProxyDisabled, + SetSystemProxyPac, + SetSystemProxyGlobal, + SetTrafficUser, + SetTrafficAdmin, + SetDnsSystem, + SetDnsDirect, + SetDnsProxy, + SetDnsDnsCrypt, + CheckDnsCryptUpdate, + SelectServer, + SelectStrategy, + UseLocalPac, + UseOnlinePac, + UpdateLocalPacFromGeosite, + EditUserRuleFile, + ToggleSecureLocalPac, + ToggleRegeneratePacOnUpdate, + EditOnlinePacUrl, + ToggleStartWithWindows, + ToggleProtocolHandler, + ToggleAllowLan, + CheckUpdates, + TogglePreRelease, + Exit, +} + +public sealed record TrayServerMenuItem(int Index, string Text, bool IsChecked); +public sealed record TrayStrategyMenuItem(string Id, string Text, bool IsChecked); + +public sealed record TrayMenuState( + string Tooltip, + TraySystemProxyMode SystemProxyMode, + TrayTrafficMode TrafficMode, + string TrafficStatusText, + TrayDnsMode DnsMode, + string DnsStatusText, + bool DnsCryptInstalled, + bool DnsCryptUpdateAvailable, + IReadOnlyList Strategies, + IReadOnlyList Servers, + bool HasConfiguredServer, + bool UseOnlinePac, + bool SecureLocalPac, + bool RegeneratePacOnUpdate, + bool StartWithWindows, + bool StartWithWindowsAvailable, + bool ProtocolHandlerEnabled, + bool AllowLan, + bool CheckPreRelease, + int TotalServerCount); + +public sealed class TrayCommandEventArgs : EventArgs +{ + public TrayCommandEventArgs(TrayCommandKind command, int index = -1, string? value = null) + { + Command = command; + Index = index; + Value = value; + } + + public TrayCommandKind Command { get; } + public int Index { get; } + public string? Value { get; } +} + +/// +/// WinUI 3 notification-area adapter backed by WinUIEx.TrayIcon. +/// It mirrors the original Shadowsocks tray hierarchy while keeping all controller +/// decisions in the application layer through a plain command/state boundary. +/// +public sealed class TrayIconService : IDisposable +{ + private const uint TrayIconId = 1; + private const int MaxTooltipLength = 127; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly TrayIcon _trayIcon; + private readonly Func _localize; + private TrayMenuState _state; + private nint _baseIconHandle; + private nint _inIconHandle; + private nint _outIconHandle; + private nint _bothIconHandle; + private bool _hasInboundActivity; + private bool _hasOutboundActivity; + private bool _disposed; + + public TrayIconService( + string executablePath, + TrayMenuState initialState, + Func? localize = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(executablePath); + ArgumentNullException.ThrowIfNull(initialState); + + _state = initialState; + _localize = localize ?? (text => text); + + (_baseIconHandle, _inIconHandle, _outIconHandle, _bothIconHandle) = CreateStateIcons(_state); + _trayIcon = new TrayIcon( + TrayIconId, + new IconId((ulong)_baseIconHandle), + NormalizeTooltip(initialState.Tooltip)); + _trayIcon.LeftDoubleClick += OnLeftDoubleClick; + _trayIcon.ContextMenu += OnContextMenu; + _trayIcon.IsVisible = true; + ApplyActivityIcon(); + + Logger.Info("Shadowsocks notification-area icon registered through WinUIEx using embedded tray assets."); + } + + public event EventHandler? CommandRequested; + + public bool IsRegistered => !_disposed && _trayIcon.IsVisible; + + public void UpdateState(TrayMenuState state) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(state); + bool pacSourceChanged = state.SystemProxyMode == TraySystemProxyMode.Pac + && _state.UseOnlinePac != state.UseOnlinePac; + bool visualStateChanged = _state.SystemProxyMode != state.SystemProxyMode || pacSourceChanged; + _state = state; + _trayIcon.Tooltip = NormalizeTooltip(state.Tooltip); + if (visualStateChanged) + { + RebuildStateIcons(); + } + } + + public void UpdateActivity(bool hasInbound, bool hasOutbound) + { + ThrowIfDisposed(); + if (_hasInboundActivity == hasInbound && _hasOutboundActivity == hasOutbound) + { + return; + } + + _hasInboundActivity = hasInbound; + _hasOutboundActivity = hasOutbound; + ApplyActivityIcon(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _trayIcon.LeftDoubleClick -= OnLeftDoubleClick; + _trayIcon.ContextMenu -= OnContextMenu; + _trayIcon.Dispose(); + ReleaseStateIcons(); + Logger.Info("Shadowsocks WinUIEx notification-area icon disposed."); + } + + private void OnLeftDoubleClick(TrayIcon _, TrayIconEventArgs _1) + => Raise(TrayCommandKind.OpenServers); + + private void OnContextMenu(TrayIcon _, TrayIconEventArgs args) + { + TrayMenuState state = _state; + var flyout = new MenuFlyout(); + + flyout.Items.Add(BuildSystemProxyMenu(state)); + flyout.Items.Add(BuildTrafficMenu(state)); + flyout.Items.Add(BuildServersMenu(state)); + flyout.Items.Add(CreateItem("Plugins", TrayCommandKind.OpenPlugins)); + flyout.Items.Add(BuildDnsMenu(state)); + flyout.Items.Add(BuildPacMenu(state)); + flyout.Items.Add(new MenuFlyoutSeparator()); + flyout.Items.Add(CreateItem("Forward Proxy", TrayCommandKind.OpenForwardProxy)); + flyout.Items.Add(CreateItem("Online Config", TrayCommandKind.OpenOnlineConfig)); + flyout.Items.Add(new MenuFlyoutSeparator()); + ToggleMenuFlyoutItem startupItem = CreateToggleItem("Start on Boot", state.StartWithWindows, TrayCommandKind.ToggleStartWithWindows); + startupItem.IsEnabled = state.StartWithWindowsAvailable; + flyout.Items.Add(startupItem); + flyout.Items.Add(CreateToggleItem("Associate ss:// Links", state.ProtocolHandlerEnabled, TrayCommandKind.ToggleProtocolHandler)); + flyout.Items.Add(CreateToggleItem("Allow other devices to connect", state.AllowLan, TrayCommandKind.ToggleAllowLan)); + flyout.Items.Add(new MenuFlyoutSeparator()); + flyout.Items.Add(CreateItem("Hotkeys", TrayCommandKind.OpenHotkeys)); + flyout.Items.Add(BuildHelpMenu(state)); + flyout.Items.Add(new MenuFlyoutSeparator()); + flyout.Items.Add(CreateItem("Quit", TrayCommandKind.Exit)); + + args.Flyout = flyout; + } + + private MenuFlyoutSubItem BuildSystemProxyMenu(TrayMenuState state) + { + var menu = new MenuFlyoutSubItem { Text = L("System Proxy") }; + menu.Items.Add(CreateToggleItem("Disable", state.SystemProxyMode == TraySystemProxyMode.Disabled, TrayCommandKind.SetSystemProxyDisabled)); + ToggleMenuFlyoutItem pac = CreateToggleItem("PAC", state.SystemProxyMode == TraySystemProxyMode.Pac, TrayCommandKind.SetSystemProxyPac); + ToggleMenuFlyoutItem global = CreateToggleItem("Global", state.SystemProxyMode == TraySystemProxyMode.Global, TrayCommandKind.SetSystemProxyGlobal); + pac.IsEnabled = state.HasConfiguredServer; + global.IsEnabled = state.HasConfiguredServer; + menu.Items.Add(pac); + menu.Items.Add(global); + return menu; + } + + private MenuFlyoutSubItem BuildTrafficMenu(TrayMenuState state) + { + var menu = new MenuFlyoutSubItem { Text = L("Traffic Mode") }; + menu.Items.Add(CreateToggleItem("User Mode", state.TrafficMode == TrayTrafficMode.User, TrayCommandKind.SetTrafficUser)); + + ToggleMenuFlyoutItem adminModeItem = CreateToggleItem( + "Admin Mode", + state.TrafficMode == TrayTrafficMode.Admin, + TrayCommandKind.SetTrafficAdmin); + adminModeItem.Icon = new FontIcon + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uEA18", // Shield: marks the UAC/elevation path. + }; + menu.Items.Add(adminModeItem); + + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(new MenuFlyoutItem { Text = state.TrafficStatusText, IsEnabled = false }); + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(CreateItem("Traffic Routing", TrayCommandKind.OpenTraffic)); + return menu; + } + + private MenuFlyoutSubItem BuildServersMenu(TrayMenuState state) + { + var menu = new MenuFlyoutSubItem { Text = L("Servers") }; + + foreach (TrayStrategyMenuItem strategy in state.Strategies) + { + menu.Items.Add(CreateToggleItem(strategy.Text, strategy.IsChecked, TrayCommandKind.SelectStrategy, value: strategy.Id, localize: false)); + } + + if (state.Strategies.Count > 0) + { + menu.Items.Add(new MenuFlyoutSeparator()); + } + + foreach (TrayServerMenuItem server in state.Servers) + { + menu.Items.Add(CreateToggleItem(server.Text, server.IsChecked, TrayCommandKind.SelectServer, server.Index, localize: false)); + } + + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(CreateItem("Servers", TrayCommandKind.OpenServers)); + menu.Items.Add(new MenuFlyoutSeparator()); + if (state.TotalServerCount > 20) + { + menu.Items.Add(CreateItem(LF("More than 20 servers (total: {0})", state.TotalServerCount), TrayCommandKind.OpenServers, localize: false)); + } + MenuFlyoutItem share = CreateItem("Share Server Config", TrayCommandKind.OpenSharing); + share.IsEnabled = state.HasConfiguredServer; + menu.Items.Add(share); + return menu; + } + + private MenuFlyoutSubItem BuildDnsMenu(TrayMenuState state) + { + var menu = new MenuFlyoutSubItem { Text = L("DNS"), IsEnabled = state.HasConfiguredServer }; + menu.Items.Add(CreateToggleItem("System DNS", state.DnsMode == TrayDnsMode.System, TrayCommandKind.SetDnsSystem)); + menu.Items.Add(CreateToggleItem("Direct DNS", state.DnsMode == TrayDnsMode.Direct, TrayCommandKind.SetDnsDirect)); + menu.Items.Add(CreateToggleItem("DNS through Shadowsocks", state.DnsMode == TrayDnsMode.Proxy, TrayCommandKind.SetDnsProxy)); + menu.Items.Add(CreateToggleItem("Custom DoH", state.DnsMode == TrayDnsMode.CustomDoh, TrayCommandKind.OpenDns)); + menu.Items.Add(CreateToggleItem("DNSCrypt", state.DnsMode == TrayDnsMode.DnsCrypt, TrayCommandKind.SetDnsDnsCrypt)); + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(new MenuFlyoutItem { Text = state.DnsStatusText, IsEnabled = false }); + if (state.DnsCryptInstalled) + { + MenuFlyoutItem updateItem = CreateItem("Check DNSCrypt Update", TrayCommandKind.CheckDnsCryptUpdate); + if (state.DnsCryptUpdateAvailable) + { + updateItem.Icon = new FontIcon + { + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"), + Glyph = "\uE896", + }; + } + menu.Items.Add(updateItem); + } + menu.Items.Add(CreateItem("DNS Settings", TrayCommandKind.OpenDns)); + return menu; + } + + private MenuFlyoutSubItem BuildPacMenu(TrayMenuState state) + { + // Keep PAC menu actions consistent with the current Local/Online PAC state. + bool localPac = !state.UseOnlinePac; + + var menu = new MenuFlyoutSubItem { Text = L("PAC"), IsEnabled = state.HasConfiguredServer }; + menu.Items.Add(CreateToggleItem("Local PAC", localPac, TrayCommandKind.UseLocalPac)); + menu.Items.Add(CreateToggleItem("Online PAC", state.UseOnlinePac, TrayCommandKind.UseOnlinePac)); + menu.Items.Add(new MenuFlyoutSeparator()); + + if (localPac) + { + menu.Items.Add(CreateItem("Edit Local User Rules", TrayCommandKind.EditUserRuleFile)); + menu.Items.Add(CreateItem("GeoSite Sources", TrayCommandKind.OpenPac)); + menu.Items.Add(CreateItem("Update Local PAC from Geosite", TrayCommandKind.UpdateLocalPacFromGeosite)); + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(CreateToggleItem("Require secret for local PAC URL", state.SecureLocalPac, TrayCommandKind.ToggleSecureLocalPac)); + menu.Items.Add(CreateToggleItem("Regenerate Local PAC after application updates", state.RegeneratePacOnUpdate, TrayCommandKind.ToggleRegeneratePacOnUpdate)); + } + else + { + menu.Items.Add(CreateItem("Edit Online PAC URL", TrayCommandKind.EditOnlinePacUrl)); + } + + return menu; + } + + private MenuFlyoutSubItem BuildHelpMenu(TrayMenuState state) + { + var help = new MenuFlyoutSubItem { Text = L("Help") }; + help.Items.Add(CreateItem("Logs", TrayCommandKind.OpenLogs)); + + var updates = new MenuFlyoutSubItem { Text = L("Updates") }; + updates.Items.Add(CreateItem("Check for Updates", TrayCommandKind.CheckUpdates)); + updates.Items.Add(new MenuFlyoutSeparator()); + updates.Items.Add(CreateToggleItem("Include prerelease versions", state.CheckPreRelease, TrayCommandKind.TogglePreRelease)); + help.Items.Add(updates); + help.Items.Add(CreateItem("About", TrayCommandKind.OpenAbout)); + return help; + } + + private MenuFlyoutItem CreateItem( + string text, + TrayCommandKind command, + int index = -1, + string? value = null, + bool localize = true) + { + var item = new MenuFlyoutItem + { + Text = localize ? L(text) : text, + }; + item.Click += (_, _) => Raise(command, index, value); + return item; + } + + private ToggleMenuFlyoutItem CreateToggleItem( + string text, + bool isChecked, + TrayCommandKind command, + int index = -1, + string? value = null, + bool localize = true) + { + var item = new ToggleMenuFlyoutItem + { + Text = localize ? L(text) : text, + IsChecked = isChecked, + }; + item.Click += (_, _) => Raise(command, index, value); + return item; + } + + private void Raise(TrayCommandKind command, int index = -1, string? value = null) + => CommandRequested?.Invoke(this, new TrayCommandEventArgs(command, index, value)); + + private string L(string text) => _localize(text); + + private string LF(string text, params object[] args) + => string.Format(System.Globalization.CultureInfo.CurrentCulture, L(text), args); + + private void RebuildStateIcons() + { + nint oldBase = _baseIconHandle; + nint oldIn = _inIconHandle; + nint oldOut = _outIconHandle; + nint oldBoth = _bothIconHandle; + + (_baseIconHandle, _inIconHandle, _outIconHandle, _bothIconHandle) = CreateStateIcons(_state); + ApplyActivityIcon(); + + DestroyIconIfNeeded(oldBase); + DestroyIconIfNeeded(oldIn); + DestroyIconIfNeeded(oldOut); + DestroyIconIfNeeded(oldBoth); + } + + private static (nint Base, nint In, nint Out, nint Both) CreateStateIcons(TrayMenuState state) + { + // Restrained, high-separation tray palette. The old bright red/orange/green/blue + // scheme was visually noisy and adjacent states looked too similar at 16-24 px. + // Disabled = graphite, Local PAC = bronze, Online PAC = forest teal, Global = navy. + Color mask = state.SystemProxyMode switch + { + TraySystemProxyMode.Global => Color.FromArgb(255, 45, 84, 128), // #2D5480 + TraySystemProxyMode.Pac when state.UseOnlinePac => Color.FromArgb(255, 42, 112, 91), // #2A705B + TraySystemProxyMode.Pac => Color.FromArgb(255, 166, 105, 38), // #A66926 + _ => Color.FromArgb(255, 91, 101, 115), // #5B6573 + }; + int size = SelectIconSize(); + + using Bitmap fill = LoadTrayAsset("ss32Fill.png"); + using Bitmap outline = LoadTrayAsset("ss32Outline.png"); + using Bitmap inbound = LoadTrayAsset("ss32In.png"); + using Bitmap outbound = LoadTrayAsset("ss32Out.png"); + using Bitmap colored = ChangeBitmapColor(fill, mask); + using Bitmap baseBitmap = AddBitmapOverlay(colored, outline); + using Bitmap inBitmap = AddBitmapOverlay(baseBitmap, inbound); + using Bitmap outBitmap = AddBitmapOverlay(baseBitmap, outbound); + using Bitmap bothBitmap = AddBitmapOverlay(baseBitmap, inbound, outbound); + using Bitmap resizedBase = ResizeBitmap(baseBitmap, size, size); + using Bitmap resizedIn = ResizeBitmap(inBitmap, size, size); + using Bitmap resizedOut = ResizeBitmap(outBitmap, size, size); + using Bitmap resizedBoth = ResizeBitmap(bothBitmap, size, size); + + return ( + resizedBase.GetHicon(), + resizedIn.GetHicon(), + resizedOut.GetHicon(), + resizedBoth.GetHicon()); + } + + private void ApplyActivityIcon() + { + nint handle = _hasInboundActivity && _hasOutboundActivity ? _bothIconHandle + : _hasInboundActivity ? _inIconHandle + : _hasOutboundActivity ? _outIconHandle + : _baseIconHandle; + if (handle != nint.Zero) + { + _trayIcon.SetIcon(new IconId((ulong)handle)); + } + } + + private void ReleaseStateIcons() + { + DestroyIconIfNeeded(_baseIconHandle); + DestroyIconIfNeeded(_inIconHandle); + DestroyIconIfNeeded(_outIconHandle); + DestroyIconIfNeeded(_bothIconHandle); + _baseIconHandle = _inIconHandle = _outIconHandle = _bothIconHandle = nint.Zero; + } + + private static void DestroyIconIfNeeded(nint handle) + { + if (handle != nint.Zero) + { + _ = DestroyIcon(handle); + } + } + + private static Bitmap LoadTrayAsset(string suffix) + { + Assembly assembly = typeof(TrayIconService).Assembly; + string? name = assembly.GetManifestResourceNames() + .FirstOrDefault(resource => resource.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)); + if (name is null) + { + throw new InvalidOperationException($"Embedded tray asset not found: {suffix}"); + } + using Stream stream = assembly.GetManifestResourceStream(name) + ?? throw new InvalidOperationException($"Embedded tray asset stream not found: {name}"); + using var source = new Bitmap(stream); + return new Bitmap(source); + } + + private static Bitmap AddBitmapOverlay(Bitmap original, params Bitmap[] overlays) + { + var bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format32bppArgb); + using Graphics canvas = Graphics.FromImage(bitmap); + canvas.DrawImage(original, Point.Empty); + foreach (Bitmap overlay in overlays) + { + using var resizedOverlay = new Bitmap(overlay, original.Size); + canvas.DrawImage(resizedOverlay, Point.Empty); + } + return bitmap; + } + + private static Bitmap ChangeBitmapColor(Bitmap original, Color colorMask) + { + var bitmap = new Bitmap(original); + for (int x = 0; x < bitmap.Width; x++) + { + for (int y = 0; y < bitmap.Height; y++) + { + Color color = original.GetPixel(x, y); + if (color.A == 0) + { + continue; + } + bitmap.SetPixel(x, y, Color.FromArgb( + color.A * colorMask.A / 255, + color.R * colorMask.R / 255, + color.G * colorMask.G / 255, + color.B * colorMask.B / 255)); + } + } + return bitmap; + } + + private static Bitmap ResizeBitmap(Bitmap original, int width, int height) + { + var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.DrawImage(original, new Rectangle(0, 0, width, height)); + return bitmap; + } + + private static int SelectIconSize() + { + using Graphics graphics = Graphics.FromHwnd(nint.Zero); + int dpi = (int)graphics.DpiX; + return dpi < 97 ? 16 : dpi < 121 ? 20 : dpi < 145 ? 24 : 28; + } + + [DllImport("user32.dll", SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyIcon(nint iconHandle); + + private static string NormalizeTooltip(string? tooltip) + { + string value = string.IsNullOrWhiteSpace(tooltip) + ? "shadowsocks-reborn" + : tooltip.Replace("\0", string.Empty, StringComparison.Ordinal).Trim(); + + return value.Length <= MaxTooltipLength + ? value + : value[..MaxTooltipLength]; + } + + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(_disposed, this); +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptBootstrapPolicy.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptBootstrapPolicy.cs new file mode 100644 index 000000000..1b1b7d55b --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptBootstrapPolicy.cs @@ -0,0 +1,61 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Service +{ + internal static class DnsCryptBootstrapPolicy + { + internal static IReadOnlyList GetUnsafeEndpoints( + Configuration? configuration, + DnsCryptConfig? dnsCryptConfig = null) + { + DnsCryptConfig? effectiveConfig = dnsCryptConfig ?? configuration?.dnsPolicy?.dnsCrypt; + if (effectiveConfig?.routeThroughShadowsocks != true) + return Array.Empty(); + + var unsafeEndpoints = new List(); + foreach (Server server in configuration?.configs ?? []) + { + if (server?.IsConfigured != true) + continue; + + string host = server.server?.Trim() ?? string.Empty; + if (!IPAddress.TryParse(host, out _)) + unsafeEndpoints.Add(host); + } + + ForwardProxyConfig? forwardProxy = configuration?.proxy; + if (forwardProxy?.useProxy == true) + { + string proxyHost = forwardProxy.proxyServer?.Trim() ?? string.Empty; + if (!IPAddress.TryParse(proxyHost, out _)) + unsafeEndpoints.Add(proxyHost); + } + + return unsafeEndpoints + .Where(endpoint => !string.IsNullOrWhiteSpace(endpoint)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(endpoint => endpoint, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + internal static void Validate( + Configuration? configuration, + DnsCryptConfig? dnsCryptConfig = null) + { + IReadOnlyList unsafeEndpoints = GetUnsafeEndpoints(configuration, dnsCryptConfig); + if (unsafeEndpoints.Count == 0) + return; + + throw new InvalidOperationException( + "Routing DNSCrypt through Shadowsocks requires IP-address Shadowsocks/forward-proxy endpoints " + + "to prevent a DNS bootstrap recursion loop. Hostname endpoint(s): " + + string.Join(", ", unsafeEndpoints)); + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.Package.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.Package.cs new file mode 100644 index 000000000..eb2a2e504 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.Package.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller.Service +{ + public sealed partial class DnsCryptComponentManager + { + internal static DnsCryptReleaseInfo SelectRelease(GitHubRelease release) + { + ArgumentNullException.ThrowIfNull(release); + if (release.Draft) + throw new InvalidDataException("Draft DNSCrypt releases cannot be installed."); + if (release.Prerelease) + throw new InvalidDataException("Prerelease DNSCrypt releases cannot be installed by the stable component channel."); + + Version tagVersion = ParseReleaseTag(release.TagName) + ?? throw new InvalidDataException($"Unsupported DNSCrypt release tag '{release.TagName}'."); + GitHubAsset[] assets = release.Assets ?? []; + GitHubAsset archive = null; + Version assetVersion = null; + + foreach (GitHubAsset candidate in assets) + { + Match match = ReleaseAssetRegex.Match(candidate?.Name ?? string.Empty); + if (!match.Success) + continue; + + if (!Version.TryParse(match.Groups["version"].Value, out Version parsed)) + continue; + if (archive is not null) + throw new InvalidDataException("The DNSCrypt release contains multiple matching Windows x64 ZIP assets."); + + archive = candidate; + assetVersion = parsed; + } + + if (archive is null || assetVersion is null) + throw new InvalidDataException("The DNSCrypt release does not contain the expected Windows x64 ZIP asset."); + if (!assetVersion.Equals(tagVersion)) + { + throw new InvalidDataException( + $"DNSCrypt release tag {FormatVersion(tagVersion)} does not match asset version {FormatVersion(assetVersion)}."); + } + + string signatureName = archive.Name + ".minisig"; + GitHubAsset[] signatures = assets.Where(candidate => + string.Equals(candidate?.Name, signatureName, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (signatures.Length != 1) + throw new InvalidDataException($"The DNSCrypt release must contain exactly one signature asset '{signatureName}'."); + GitHubAsset signature = signatures[0]; + + Uri archiveUri = ValidateOfficialReleaseDownloadUri(archive.DownloadUrl, archive.Name, tagVersion); + Uri signatureUri = ValidateOfficialReleaseDownloadUri(signature.DownloadUrl, signature.Name, tagVersion); + string digest = NormalizeSha256Digest(archive.Digest); + if (archive.Size < 0 || archive.Size > MaxArchiveDownloadBytes) + throw new InvalidDataException("The DNSCrypt archive size reported by GitHub is outside the allowed range."); + + return new DnsCryptReleaseInfo( + tagVersion, + release.TagName, + archive.Name, + archiveUri, + signature.Name, + signatureUri, + archive.Size, + digest); + } + + internal static void ExtractZipSafely(string zipPath, string destinationDirectory) + { + if (!File.Exists(zipPath)) + throw new FileNotFoundException("DNSCrypt archive was not found.", zipPath); + + Directory.CreateDirectory(destinationDirectory); + string root = EnsureTrailingSeparator(Path.GetFullPath(destinationDirectory)); + long totalExtracted = 0; + int entryCount = 0; + + using FileStream stream = new(zipPath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false); + foreach (ZipArchiveEntry entry in archive.Entries) + { + entryCount++; + if (entryCount > MaxArchiveEntries) + throw new InvalidDataException($"DNSCrypt archive contains more than {MaxArchiveEntries} entries."); + ValidateZipEntryType(entry); + + string name = entry.FullName ?? string.Empty; + if (string.IsNullOrWhiteSpace(name)) + continue; + if (Path.IsPathRooted(name) + || name.StartsWith('/') + || name.StartsWith('\\') + || name.Contains(':')) + { + throw new InvalidDataException($"Unsafe DNSCrypt archive entry '{name}'."); + } + + string normalizedName = name + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + string target = Path.GetFullPath(Path.Combine(root, normalizedName)); + if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException($"DNSCrypt archive entry escapes the extraction directory: '{name}'."); + + bool directoryEntry = name.EndsWith('/') + || name.EndsWith('\\'); + if (directoryEntry) + { + Directory.CreateDirectory(target); + continue; + } + + if (entry.Length < 0 || entry.Length > MaxSingleExtractedFileBytes) + throw new InvalidDataException($"DNSCrypt archive entry '{name}' is too large."); + totalExtracted = checked(totalExtracted + entry.Length); + if (totalExtracted > MaxExtractedBytes) + throw new InvalidDataException("DNSCrypt archive expands beyond the allowed size."); + + string parent = Path.GetDirectoryName(target); + if (!string.IsNullOrWhiteSpace(parent)) + Directory.CreateDirectory(parent); + + using Stream input = entry.Open(); + using FileStream output = new(target, FileMode.CreateNew, FileAccess.Write, FileShare.None); + CopyWithLimit(input, output, entry.Length, MaxSingleExtractedFileBytes); + } + } + + private async Task PrepareReleaseCoreAsync( + DnsCryptReleaseInfo release, + IProgress progress, + bool forceDownload, + CancellationToken cancellationToken) + { + string existingExecutable = GetExecutablePath(release.Version); + if (!forceDownload && File.Exists(existingExecutable)) + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Prepared)); + return new DnsCryptPreparedComponent( + release.Version, + GetVersionDirectory(release.Version), + existingExecutable); + } + + Directory.CreateDirectory(updateDirectory); + Directory.CreateDirectory(componentDirectory); + string operationDirectory = Path.Combine(updateDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(operationDirectory); + string archivePath = Path.Combine(operationDirectory, release.ArchiveName); + string signaturePath = Path.Combine(operationDirectory, release.SignatureName); + string extractionDirectory = Path.Combine(operationDirectory, "extract"); + + try + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.DownloadingArchive)); + await DownloadFileAsync( + release.ArchiveDownloadUri, + archivePath, + MaxArchiveDownloadBytes, + DnsCryptComponentStage.DownloadingArchive, + progress, + cancellationToken).ConfigureAwait(false); + if (release.ArchiveSize > 0 && new FileInfo(archivePath).Length != release.ArchiveSize) + throw new InvalidDataException("Downloaded DNSCrypt archive size does not match GitHub release metadata."); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.DownloadingSignature)); + await DownloadFileAsync( + release.SignatureDownloadUri, + signaturePath, + MaxSignatureDownloadBytes, + DnsCryptComponentStage.DownloadingSignature, + progress, + cancellationToken).ConfigureAwait(false); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Verifying)); + VerifySha256IfAvailable(archivePath, release.Sha256Digest); + string signatureText = await File.ReadAllTextAsync(signaturePath, Encoding.UTF8, cancellationToken).ConfigureAwait(false); + bool validSignature = trustedReleasePublicKeys.Any(key => + { + try + { + return MinisignVerifier.VerifyFile(archivePath, signatureText, key); + } + catch (InvalidDataException) + { + return false; + } + }); + if (!validSignature) + throw new CryptographicException("DNSCrypt Proxy release signature verification failed."); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Installing)); + Directory.CreateDirectory(extractionDirectory); + ExtractZipSafely(archivePath, extractionDirectory); + string[] executables = Directory + .EnumerateFiles(extractionDirectory, ExpectedExecutableName, SearchOption.AllDirectories) + .ToArray(); + if (executables.Length != 1) + { + throw new InvalidDataException( + $"DNSCrypt archive must contain exactly one '{ExpectedExecutableName}' file, but {executables.Length} were found."); + } + + string versionDirectory = GetVersionDirectory(release.Version); + bool preserveExistingVersion = forceDownload && Directory.Exists(versionDirectory); + string preparedDirectory = preserveExistingVersion + ? Path.Combine(componentDirectory, $".prepared-{FormatVersion(release.Version)}-{Guid.NewGuid():N}") + : versionDirectory; + string installStaging = Path.Combine(componentDirectory, $".install-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(installStaging); + string stagedExecutable = Path.Combine(installStaging, ExpectedExecutableName); + File.Copy(executables[0], stagedExecutable, overwrite: false); + + if (Directory.Exists(preparedDirectory)) + throw new IOException($"DNSCrypt prepared directory already exists: {preparedDirectory}"); + Directory.Move(installStaging, preparedDirectory); + } + catch + { + TryDeleteDirectory(installStaging); + if (preserveExistingVersion) + TryDeleteDirectory(preparedDirectory); + throw; + } + + string preparedExecutable = Path.Combine(preparedDirectory, ExpectedExecutableName); + if (!File.Exists(preparedExecutable)) + throw new InvalidDataException("DNSCrypt component preparation did not produce the expected executable."); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Prepared)); + return new DnsCryptPreparedComponent(release.Version, preparedDirectory, preparedExecutable); + } + finally + { + TryDeleteDirectory(operationDirectory); + } + } + + private void ActivateVersionCore(Version version) + { + string executable = GetExecutablePath(version); + if (!File.Exists(executable)) + throw new InvalidOperationException($"DNSCrypt Proxy {FormatVersion(version)} is not prepared."); + + ComponentMetadata metadata = ReadMetadata() ?? CreateDefaultMetadata(); + Version active = ParseStoredVersion(metadata.ActiveVersion); + if (active is null || active != version) + { + metadata.PreviousVersion = active is null ? null : FormatVersion(active); + metadata.ActiveVersion = FormatVersion(version); + } + WriteMetadata(metadata); + CleanupUnusedVersions(metadata.ActiveVersion, metadata.PreviousVersion); + } + + private async Task DownloadFileAsync( + Uri uri, + string destination, + long maxBytes, + DnsCryptComponentStage stage, + IProgress progress, + CancellationToken cancellationToken) + { + try + { + using HttpRequestMessage request = CreateDownloadRequest(HttpMethod.Get, uri); + using HttpResponseMessage response = await httpClient.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + long? contentLength = response.Content.Headers.ContentLength; + if (contentLength.HasValue && contentLength.Value > maxBytes) + throw new InvalidDataException($"DNSCrypt download exceeds the {maxBytes}-byte safety limit."); + + await using Stream input = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using FileStream output = new(destination, FileMode.CreateNew, FileAccess.Write, FileShare.None); + byte[] buffer = new byte[64 * 1024]; + long transferred = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + + transferred = checked(transferred + read); + if (transferred > maxBytes) + throw new InvalidDataException($"DNSCrypt download exceeds the {maxBytes}-byte safety limit."); + + await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + progress?.Report(new DnsCryptComponentProgress(stage, transferred, contentLength)); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or DnsCryptBootstrapException) + { + Logger.Error(exception, "DNSCrypt release asset download failed through DoH-over-Shadowsocks: {0}", uri); + throw new DnsCryptComponentNetworkException( + $"DNSCrypt component asset '{uri.Host}' could not be downloaded through DoH-over-Shadowsocks.", + exception); + } + } + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient + { + Timeout = TimeSpan.FromMinutes(5), + }; + return client; + } + + private static HttpRequestMessage CreateGitHubApiRequest(HttpMethod method, Uri uri) + { + HttpRequestMessage request = CreateDownloadRequest(method, uri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + request.Headers.Add("X-GitHub-Api-Version", "2022-11-28"); + return request; + } + + private static HttpRequestMessage CreateDownloadRequest(HttpMethod method, Uri uri) + { + var request = new HttpRequestMessage(method, uri); + request.Headers.UserAgent.ParseAdd($"Shadowsocks-Reborn/{ApplicationInfo.Version}"); + return request; + } + + private static async Task ReadContentBytesWithLimitAsync( + HttpContent content, + long maxBytes, + CancellationToken cancellationToken) + { + long? contentLength = content.Headers.ContentLength; + if (contentLength.HasValue && contentLength.Value > maxBytes) + throw new InvalidDataException($"HTTP response exceeds the {maxBytes}-byte safety limit."); + + await using Stream input = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + int initialCapacity = contentLength.HasValue && contentLength.Value > 0 && contentLength.Value <= int.MaxValue + ? (int)contentLength.Value + : 0; + using var output = new MemoryStream(initialCapacity); + byte[] buffer = new byte[32 * 1024]; + long transferred = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + + transferred = checked(transferred + read); + if (transferred > maxBytes) + throw new InvalidDataException($"HTTP response exceeds the {maxBytes}-byte safety limit."); + await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + } + return output.ToArray(); + } + + private static Uri ValidateOfficialReleaseDownloadUri(string value, string assetName, Version releaseVersion) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out Uri uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) + || !string.Equals(uri.Host, "github.com", StringComparison.OrdinalIgnoreCase) + || !string.IsNullOrEmpty(uri.Query) + || !string.IsNullOrEmpty(uri.Fragment)) + { + throw new InvalidDataException($"DNSCrypt asset '{assetName}' has an untrusted download URL."); + } + + string[] segments = uri.AbsolutePath + .Split('/', StringSplitOptions.RemoveEmptyEntries) + .Select(Uri.UnescapeDataString) + .ToArray(); + bool officialPath = segments.Length == 6 + && string.Equals(segments[0], "DNSCrypt", StringComparison.OrdinalIgnoreCase) + && string.Equals(segments[1], "dnscrypt-proxy", StringComparison.OrdinalIgnoreCase) + && string.Equals(segments[2], "releases", StringComparison.OrdinalIgnoreCase) + && string.Equals(segments[3], "download", StringComparison.OrdinalIgnoreCase) + && ParseReleaseTag(segments[4]) is Version urlVersion + && urlVersion.Equals(releaseVersion) + && string.Equals(segments[5], assetName, StringComparison.OrdinalIgnoreCase); + if (!officialPath) + throw new InvalidDataException($"DNSCrypt asset '{assetName}' points outside the expected official release path."); + + return uri; + } + + private static Version ParseReleaseTag(string tagName) + { + if (string.IsNullOrWhiteSpace(tagName)) + return null; + string normalized = tagName.Trim(); + if (normalized.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + normalized = normalized.Substring(1); + + string[] parts = normalized.Split('.'); + if (parts.Length != 3 || parts.Any(part => !int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out _))) + return null; + return Version.TryParse(normalized, out Version version) ? version : null; + } + + private static string NormalizeSha256Digest(string digest) + { + if (string.IsNullOrWhiteSpace(digest)) + return null; + + const string prefix = "sha256:"; + if (!digest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("DNSCrypt release contains an unsupported GitHub asset digest."); + + string hex = digest.Substring(prefix.Length).Trim(); + if (hex.Length != 64 || !hex.All(Uri.IsHexDigit)) + throw new InvalidDataException("DNSCrypt release contains a malformed SHA-256 digest."); + return hex.ToLowerInvariant(); + } + + private static void VerifySha256IfAvailable(string filePath, string expectedHex) + { + if (string.IsNullOrWhiteSpace(expectedHex)) + return; + + byte[] expected = Convert.FromHexString(expectedHex); + byte[] actual; + using (FileStream input = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (SHA256 sha256 = SHA256.Create()) + { + actual = sha256.ComputeHash(input); + } + + if (!CryptographicOperations.FixedTimeEquals(actual, expected)) + throw new CryptographicException("DNSCrypt Proxy archive SHA-256 digest does not match GitHub release metadata."); + } + + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.cs new file mode 100644 index 000000000..32aef1fc8 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentManager.cs @@ -0,0 +1,857 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Service +{ + internal sealed class DnsCryptActivationLease + { + internal DnsCryptActivationLease( + Version version, + string targetDirectory, + string backupDirectory, + bool hadMetadata, + string originalActiveVersion, + string originalPreviousVersion, + bool originalAutoUpdate, + DateTimeOffset? originalLastUpdateCheckUtc) + { + Version = version; + TargetDirectory = targetDirectory; + BackupDirectory = backupDirectory; + HadMetadata = hadMetadata; + OriginalActiveVersion = originalActiveVersion; + OriginalPreviousVersion = originalPreviousVersion; + OriginalAutoUpdate = originalAutoUpdate; + OriginalLastUpdateCheckUtc = originalLastUpdateCheckUtc; + } + + internal Version Version { get; } + internal string TargetDirectory { get; } + internal string BackupDirectory { get; } + internal bool HadMetadata { get; } + internal string OriginalActiveVersion { get; } + internal string OriginalPreviousVersion { get; } + internal bool OriginalAutoUpdate { get; } + internal DateTimeOffset? OriginalLastUpdateCheckUtc { get; } + internal bool Completed { get; set; } + } + + public sealed partial class DnsCryptComponentManager : IDisposable + { + public const string ComponentId = "dnscrypt-proxy"; + public const string Repository = "DNSCrypt/dnscrypt-proxy"; + public const string ExpectedExecutableName = "dnscrypt-proxy.exe"; + public const string ReleaseSigningPublicKey = "RWTk1xXqcTODeYttYMCMLo0YJHaFEHn7a3akqHlb/7QvIQXHVPxKbjB5"; + + private const string LatestReleaseApiUrl = "https://api.github.com/repos/DNSCrypt/dnscrypt-proxy/releases/latest"; + private const string MetadataFileName = "component.json"; + private const int MaxArchiveEntries = 512; + private const long MaxReleaseMetadataBytes = 2L * 1024 * 1024; + private const long MaxArchiveDownloadBytes = 128L * 1024 * 1024; + private const long MaxSignatureDownloadBytes = 64L * 1024; + private const long MaxExtractedBytes = 256L * 1024 * 1024; + private const long MaxSingleExtractedFileBytes = 128L * 1024 * 1024; + private static readonly Regex ReleaseAssetRegex = new( + @"^dnscrypt-proxy-win64-(?[0-9]+\.[0-9]+\.[0-9]+)\.zip$", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly JsonSerializerOptions MetadataJsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly HttpClient httpClient; + private readonly bool ownsHttpClient; + private readonly string componentDirectory; + private readonly string updateDirectory; + private readonly string[] trustedReleasePublicKeys; + private readonly SemaphoreSlim operationLock = new(1, 1); + private bool disposed; + + public DnsCryptComponentManager(HttpClient httpClient = null) + : this( + httpClient, + AppStoragePaths.DnsCryptComponentDirectory, + AppStoragePaths.DnsCryptUpdateDirectory, + [ReleaseSigningPublicKey], + ownsHttpClient: httpClient is null) + { + } + + internal DnsCryptComponentManager( + Func proxyHostProvider, + Func proxyPortProvider) + : this( + ShadowsocksDohHttpClient.CreateTunneledClient( + proxyHostProvider, + proxyPortProvider, + TimeSpan.FromMinutes(5)), + AppStoragePaths.DnsCryptComponentDirectory, + AppStoragePaths.DnsCryptUpdateDirectory, + [ReleaseSigningPublicKey], + ownsHttpClient: true) + { + } + + internal DnsCryptComponentManager( + HttpClient httpClient, + string componentDirectory, + string updateDirectory, + IReadOnlyList trustedReleasePublicKeys) + : this(httpClient, componentDirectory, updateDirectory, trustedReleasePublicKeys, ownsHttpClient: false) + { + } + + private DnsCryptComponentManager( + HttpClient httpClient, + string componentDirectory, + string updateDirectory, + IReadOnlyList trustedReleasePublicKeys, + bool ownsHttpClient) + { + if (string.IsNullOrWhiteSpace(componentDirectory)) + throw new ArgumentException("A DNSCrypt component directory is required.", nameof(componentDirectory)); + if (string.IsNullOrWhiteSpace(updateDirectory)) + throw new ArgumentException("A DNSCrypt update directory is required.", nameof(updateDirectory)); + if (trustedReleasePublicKeys is null || trustedReleasePublicKeys.Count == 0) + throw new ArgumentException("At least one trusted DNSCrypt release signing key is required.", nameof(trustedReleasePublicKeys)); + + this.httpClient = httpClient ?? CreateHttpClient(); + this.ownsHttpClient = ownsHttpClient || httpClient is null; + this.componentDirectory = Path.GetFullPath(componentDirectory); + this.updateDirectory = Path.GetFullPath(updateDirectory); + this.trustedReleasePublicKeys = trustedReleasePublicKeys + .Where(key => !string.IsNullOrWhiteSpace(key)) + .Select(key => key.Trim()) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + if (this.trustedReleasePublicKeys.Length == 0) + throw new ArgumentException("At least one non-empty DNSCrypt release signing key is required.", nameof(trustedReleasePublicKeys)); + + CleanupTransientState(); + } + + public async Task GetLatestReleaseAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + try + { + using HttpRequestMessage request = CreateGitHubApiRequest(HttpMethod.Get, new Uri(LatestReleaseApiUrl)); + using HttpResponseMessage response = await httpClient.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + byte[] content = await ReadContentBytesWithLimitAsync( + response.Content, + MaxReleaseMetadataBytes, + cancellationToken).ConfigureAwait(false); + GitHubRelease release = JsonSerializer.Deserialize(content) + ?? throw new InvalidDataException("GitHub returned an empty DNSCrypt release document."); + + return SelectRelease(release); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or DnsCryptBootstrapException) + { + Logger.Error(exception, "DNSCrypt GitHub release lookup failed through DoH-over-Shadowsocks: {0}", LatestReleaseApiUrl); + throw new DnsCryptComponentNetworkException( + "DNSCrypt component release metadata could not be fetched through DoH-over-Shadowsocks.", + exception); + } + } + + public async Task PrepareLatestAsync( + IProgress progress = null, + bool forceDownload = false, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + await operationLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.CheckingRelease)); + DnsCryptReleaseInfo release = await GetLatestReleaseAsync(cancellationToken).ConfigureAwait(false); + return await PrepareReleaseCoreAsync(release, progress, forceDownload, cancellationToken).ConfigureAwait(false); + } + finally + { + operationLock.Release(); + } + } + + internal async Task PrepareReleaseAsync( + DnsCryptReleaseInfo release, + IProgress progress = null, + bool forceDownload = false, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(release); + await operationLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + return await PrepareReleaseCoreAsync(release, progress, forceDownload, cancellationToken).ConfigureAwait(false); + } + finally + { + operationLock.Release(); + } + } + + // Deliberately no public "download-and-activate" shortcut here. A downloaded + // DNSCrypt release is untrusted for execution until DnsCryptRuntimeManager has + // validated its reported version, generated configuration and DNS health-check. + // The controller therefore uses PrepareLatestAsync -> ValidatePreparedAsync -> + // ActivatePrepared -> runtime start/health-check -> CommitActivation. When an + // existing version directory is replaced, its backup remains rollback-capable + // until the post-activation runtime has proved healthy. + + public void ActivateVersion(Version version) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(version); + ValidateComponentVersion(version); + operationLock.Wait(); + try + { + ActivateVersionCore(version); + } + finally + { + operationLock.Release(); + } + } + + internal DnsCryptActivationLease ActivatePrepared(DnsCryptPreparedComponent prepared) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(prepared); + ArgumentNullException.ThrowIfNull(prepared.Version); + ValidateComponentVersion(prepared.Version); + ArgumentException.ThrowIfNullOrWhiteSpace(prepared.DirectoryPath); + ArgumentException.ThrowIfNullOrWhiteSpace(prepared.ExecutablePath); + + operationLock.Wait(); + try + { + string targetDirectory = GetVersionDirectory(prepared.Version); + string preparedDirectory = Path.GetFullPath(prepared.DirectoryPath); + string expectedExecutable = Path.GetFullPath(Path.Combine(preparedDirectory, ExpectedExecutableName)); + if (!string.Equals(Path.GetFullPath(prepared.ExecutablePath), expectedExecutable, StringComparison.OrdinalIgnoreCase) + || !File.Exists(expectedExecutable)) + { + throw new InvalidOperationException("The prepared DNSCrypt component is incomplete or points outside its prepared directory."); + } + + if (string.Equals( + preparedDirectory.TrimEnd(Path.DirectorySeparatorChar), + Path.GetFullPath(targetDirectory).TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)) + { + ActivateVersionCore(prepared.Version); + return null; + } + + string componentRoot = Path.GetFullPath(componentDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + if (!preparedDirectory.StartsWith(componentRoot, StringComparison.OrdinalIgnoreCase) + || !Path.GetFileName(preparedDirectory).StartsWith(".prepared-", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The prepared DNSCrypt component directory is not trusted."); + } + + ComponentMetadata originalMetadata = ReadMetadata(); + string backupDirectory = Path.Combine( + componentDirectory, + $".activate-backup-{FormatVersion(prepared.Version)}-{Guid.NewGuid():N}"); + bool movedTarget = false; + bool movedPrepared = false; + try + { + if (Directory.Exists(targetDirectory)) + { + Directory.Move(targetDirectory, backupDirectory); + movedTarget = true; + } + Directory.Move(preparedDirectory, targetDirectory); + movedPrepared = true; + ActivateVersionCore(prepared.Version); + + return movedTarget + ? new DnsCryptActivationLease( + prepared.Version, + targetDirectory, + backupDirectory, + originalMetadata is not null, + originalMetadata?.ActiveVersion, + originalMetadata?.PreviousVersion, + originalMetadata?.AutoUpdate ?? true, + originalMetadata?.LastUpdateCheckUtc) + : null; + } + catch + { + if (movedPrepared && Directory.Exists(targetDirectory)) + TryDeleteDirectory(targetDirectory); + if (movedTarget && Directory.Exists(backupDirectory) && !Directory.Exists(targetDirectory)) + Directory.Move(backupDirectory, targetDirectory); + throw; + } + } + finally + { + operationLock.Release(); + } + } + + internal void CommitActivation(DnsCryptActivationLease activation) + { + if (activation is null) + return; + ThrowIfDisposed(); + + operationLock.Wait(); + try + { + ValidateActivationLease(activation); + if (activation.Completed) + return; + + TryDeleteDirectory(activation.BackupDirectory); + activation.Completed = true; + } + finally + { + operationLock.Release(); + } + } + + internal void RollbackActivation(DnsCryptActivationLease activation) + { + ArgumentNullException.ThrowIfNull(activation); + ThrowIfDisposed(); + + operationLock.Wait(); + try + { + ValidateActivationLease(activation); + if (activation.Completed) + throw new InvalidOperationException("The DNSCrypt activation transaction has already been completed."); + if (!Directory.Exists(activation.BackupDirectory)) + throw new DirectoryNotFoundException("The DNSCrypt activation backup is no longer available."); + + if (Directory.Exists(activation.TargetDirectory)) + TryDeleteDirectory(activation.TargetDirectory); + Directory.Move(activation.BackupDirectory, activation.TargetDirectory); + + if (activation.HadMetadata) + { + ComponentMetadata restored = CreateDefaultMetadata(); + restored.ActiveVersion = activation.OriginalActiveVersion; + restored.PreviousVersion = activation.OriginalPreviousVersion; + restored.AutoUpdate = activation.OriginalAutoUpdate; + restored.LastUpdateCheckUtc = activation.OriginalLastUpdateCheckUtc; + WriteMetadata(restored); + } + else + { + TryDeleteFile(GetMetadataPath()); + } + + activation.Completed = true; + } + finally + { + operationLock.Release(); + } + } + + private void ValidateActivationLease(DnsCryptActivationLease activation) + { + ArgumentNullException.ThrowIfNull(activation.Version); + ValidateComponentVersion(activation.Version); + + string expectedTarget = Path.GetFullPath(GetVersionDirectory(activation.Version)); + string target = Path.GetFullPath(activation.TargetDirectory); + if (!string.Equals(expectedTarget, target, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("The DNSCrypt activation target is not trusted."); + + string componentRoot = Path.GetFullPath(componentDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + string backup = Path.GetFullPath(activation.BackupDirectory); + if (!backup.StartsWith(componentRoot, StringComparison.OrdinalIgnoreCase) + || !Path.GetFileName(backup).StartsWith(".activate-backup-", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The DNSCrypt activation backup is not trusted."); + } + } + + public void DiscardPrepared(DnsCryptPreparedComponent prepared) + { + ThrowIfDisposed(); + if (prepared is null) + return; + ArgumentNullException.ThrowIfNull(prepared.Version); + ValidateComponentVersion(prepared.Version); + ArgumentException.ThrowIfNullOrWhiteSpace(prepared.DirectoryPath); + + operationLock.Wait(); + try + { + ComponentMetadata metadata = ReadMetadata(); + string formatted = FormatVersion(prepared.Version); + string preparedDirectory = Path.GetFullPath(prepared.DirectoryPath); + string targetDirectory = Path.GetFullPath(GetVersionDirectory(prepared.Version)); + + if (!string.Equals( + preparedDirectory.TrimEnd(Path.DirectorySeparatorChar), + targetDirectory.TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)) + { + string componentRoot = Path.GetFullPath(componentDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + if (preparedDirectory.StartsWith(componentRoot, StringComparison.OrdinalIgnoreCase) + && Path.GetFileName(preparedDirectory).StartsWith(".prepared-", StringComparison.OrdinalIgnoreCase) + && Directory.Exists(preparedDirectory)) + { + Directory.Delete(preparedDirectory, recursive: true); + } + return; + } + + if (string.Equals(metadata?.ActiveVersion, formatted, StringComparison.OrdinalIgnoreCase) + || string.Equals(metadata?.PreviousVersion, formatted, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (Directory.Exists(targetDirectory)) + Directory.Delete(targetDirectory, recursive: true); + } + finally + { + operationLock.Release(); + } + } + + public void DiscardPreparedVersion(Version version) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(version); + ValidateComponentVersion(version); + operationLock.Wait(); + try + { + ComponentMetadata metadata = ReadMetadata(); + string formatted = FormatVersion(version); + if (string.Equals(metadata?.ActiveVersion, formatted, StringComparison.OrdinalIgnoreCase) + || string.Equals(metadata?.PreviousVersion, formatted, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + string directory = GetVersionDirectory(version); + if (Directory.Exists(directory)) + Directory.Delete(directory, recursive: true); + } + finally + { + operationLock.Release(); + } + } + + public DnsCryptPreparedComponent Rollback() + { + ThrowIfDisposed(); + operationLock.Wait(); + try + { + ComponentMetadata metadata = ReadMetadata(); + Version previous = ParseStoredVersion(metadata?.PreviousVersion); + Version active = ParseStoredVersion(metadata?.ActiveVersion); + if (previous is null) + throw new InvalidOperationException("No previous DNSCrypt Proxy version is available for rollback."); + + string previousExecutable = GetExecutablePath(previous); + if (!File.Exists(previousExecutable)) + throw new InvalidOperationException($"The previous DNSCrypt Proxy version {FormatVersion(previous)} is incomplete."); + + ComponentMetadata updated = metadata ?? CreateDefaultMetadata(); + updated.ActiveVersion = FormatVersion(previous); + updated.PreviousVersion = active is null ? null : FormatVersion(active); + WriteMetadata(updated); + CleanupUnusedVersions(updated.ActiveVersion, updated.PreviousVersion); + + return new DnsCryptPreparedComponent(previous, GetVersionDirectory(previous), previousExecutable); + } + finally + { + operationLock.Release(); + } + } + + public DnsCryptComponentStatus GetStatus() + { + ThrowIfDisposed(); + ComponentMetadata metadata = ReadMetadata(); + Version active = ParseStoredVersion(metadata?.ActiveVersion); + Version previous = ParseStoredVersion(metadata?.PreviousVersion); + if (previous is not null && !File.Exists(GetExecutablePath(previous))) + previous = null; + string executablePath = active is null ? null : GetExecutablePath(active); + bool installed = active is not null && File.Exists(executablePath); + + return new DnsCryptComponentStatus( + installed, + installed ? active : null, + previous, + installed ? executablePath : null, + metadata?.AutoUpdate ?? true, + metadata?.LastUpdateCheckUtc); + } + + public void SetAutoUpdate(bool enabled) + { + ThrowIfDisposed(); + operationLock.Wait(); + try + { + ComponentMetadata metadata = ReadMetadata() ?? CreateDefaultMetadata(); + metadata.AutoUpdate = enabled; + WriteMetadata(metadata); + } + finally + { + operationLock.Release(); + } + } + + public void RecordUpdateCheck(DateTimeOffset checkedAtUtc) + { + ThrowIfDisposed(); + operationLock.Wait(); + try + { + ComponentMetadata metadata = ReadMetadata() ?? CreateDefaultMetadata(); + metadata.LastUpdateCheckUtc = checkedAtUtc.ToUniversalTime(); + WriteMetadata(metadata); + } + finally + { + operationLock.Release(); + } + } + + public void Remove() + { + ThrowIfDisposed(); + operationLock.Wait(); + try + { + if (Directory.Exists(componentDirectory)) + Directory.Delete(componentDirectory, recursive: true); + if (Directory.Exists(updateDirectory)) + Directory.Delete(updateDirectory, recursive: true); + } + finally + { + operationLock.Release(); + } + } + + private ComponentMetadata ReadMetadata() + { + string path = GetMetadataPath(); + if (!File.Exists(path)) + return null; + + try + { + string json = File.ReadAllText(path, Encoding.UTF8); + ComponentMetadata metadata = JsonSerializer.Deserialize(json, MetadataJsonOptions); + if (metadata is null + || !string.Equals(metadata.ComponentId, ComponentId, StringComparison.Ordinal) + || !string.Equals(metadata.Repository, Repository, StringComparison.Ordinal)) + { + return null; + } + return metadata; + } + catch (JsonException) + { + return null; + } + catch (IOException) + { + return null; + } + } + + private void WriteMetadata(ComponentMetadata metadata) + { + Directory.CreateDirectory(componentDirectory); + metadata.ComponentId = ComponentId; + metadata.Repository = Repository; + string path = GetMetadataPath(); + string temporary = path + ".tmp-" + Guid.NewGuid().ToString("N"); + try + { + string json = JsonSerializer.Serialize(metadata, MetadataJsonOptions); + File.WriteAllText(temporary, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + File.Move(temporary, path, overwrite: true); + } + finally + { + TryDeleteFile(temporary); + } + } + + private static ComponentMetadata CreateDefaultMetadata() => new() + { + ComponentId = ComponentId, + Repository = Repository, + AutoUpdate = true, + }; + + private string GetMetadataPath() => Path.Combine(componentDirectory, MetadataFileName); + private string GetVersionDirectory(Version version) => Path.Combine(componentDirectory, FormatVersion(version)); + private string GetExecutablePath(Version version) => Path.Combine(GetVersionDirectory(version), ExpectedExecutableName); + + private void CleanupTransientState() + { + if (Directory.Exists(componentDirectory)) + { + foreach (string directory in Directory.EnumerateDirectories(componentDirectory)) + { + string name = Path.GetFileName(directory); + if (name.StartsWith(".prepared-", StringComparison.OrdinalIgnoreCase) + || name.StartsWith(".install-", StringComparison.OrdinalIgnoreCase)) + { + TryDeleteDirectory(directory); + continue; + } + + if (TryParseActivationBackupVersion(name, out Version backupVersion)) + { + string targetDirectory = GetVersionDirectory(backupVersion); + if (Directory.Exists(targetDirectory)) + { + TryDeleteDirectory(directory); + } + else + { + try + { + Directory.Move(directory, targetDirectory); + } + catch + { + // Keep the backup intact if crash recovery cannot restore it. + } + } + } + } + + foreach (string temporaryMetadata in Directory.EnumerateFiles( + componentDirectory, MetadataFileName + ".tmp-*", SearchOption.TopDirectoryOnly)) + { + TryDeleteFile(temporaryMetadata); + } + } + + if (Directory.Exists(updateDirectory)) + { + foreach (string directory in Directory.EnumerateDirectories(updateDirectory)) + TryDeleteDirectory(directory); + foreach (string file in Directory.EnumerateFiles(updateDirectory)) + TryDeleteFile(file); + } + } + + private void CleanupUnusedVersions(string activeVersion, string previousVersion) + { + if (!Directory.Exists(componentDirectory)) + return; + + foreach (string directory in Directory.EnumerateDirectories(componentDirectory)) + { + string name = Path.GetFileName(directory); + if (name.StartsWith(".install-", StringComparison.OrdinalIgnoreCase)) + { + TryDeleteDirectory(directory); + continue; + } + + if (string.Equals(name, activeVersion, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, previousVersion, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (Version.TryParse(name, out _)) + TryDeleteDirectory(directory); + } + } + + private static void ValidateZipEntryType(ZipArchiveEntry entry) + { + int unixType = (entry.ExternalAttributes >> 16) & 0xF000; + if (unixType != 0 && unixType != 0x4000 && unixType != 0x8000) + throw new InvalidDataException($"DNSCrypt archive contains unsupported special entry '{entry.FullName}'."); + + if ((((FileAttributes)entry.ExternalAttributes) & FileAttributes.ReparsePoint) != 0) + throw new InvalidDataException($"DNSCrypt archive contains a reparse-point entry '{entry.FullName}'."); + } + + private static void CopyWithLimit(Stream input, FileStream output, long declaredLength, long maxBytes) + { + byte[] buffer = new byte[64 * 1024]; + long copied = 0; + while (true) + { + int read = input.Read(buffer, 0, buffer.Length); + if (read == 0) + break; + + copied = checked(copied + read); + if (copied > maxBytes || copied > declaredLength) + throw new InvalidDataException("DNSCrypt archive entry expanded beyond its declared size."); + output.Write(buffer, 0, read); + } + + if (copied != declaredLength) + throw new InvalidDataException("DNSCrypt archive entry length does not match its declared size."); + } + + private static bool TryParseActivationBackupVersion(string directoryName, out Version version) + { + const string prefix = ".activate-backup-"; + version = null; + if (!directoryName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + return false; + + string remainder = directoryName[prefix.Length..]; + int separator = remainder.LastIndexOf('-'); + if (separator <= 0) + return false; + + string versionText = remainder[..separator]; + version = ParseStoredVersion(versionText); + return version is not null; + } + + private static string EnsureTrailingSeparator(string path) => + path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) + ? path + : path + Path.DirectorySeparatorChar; + + private static void ValidateComponentVersion(Version version) + { + if (version.Build < 0 || version.Revision >= 0) + throw new ArgumentException("DNSCrypt component versions must contain exactly three numeric parts.", nameof(version)); + } + + private static string FormatVersion(Version version) => $"{version.Major}.{version.Minor}.{version.Build}"; + + private static Version ParseStoredVersion(string value) => + Version.TryParse(value, out Version version) && version.Build >= 0 && version.Revision < 0 ? version : null; + + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch + { + } + } + + private static void TryDeleteDirectory(string path) + { + try + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + catch + { + } + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(disposed, this); + } + + public void Dispose() + { + if (disposed) + return; + disposed = true; + operationLock.Dispose(); + if (ownsHttpClient) + httpClient.Dispose(); + } + + internal sealed class GitHubRelease + { + [JsonPropertyName("tag_name")] + public string TagName { get; set; } + + [JsonPropertyName("draft")] + public bool Draft { get; set; } + + [JsonPropertyName("prerelease")] + public bool Prerelease { get; set; } + + [JsonPropertyName("assets")] + public GitHubAsset[] Assets { get; set; } + } + + internal sealed class GitHubAsset + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("browser_download_url")] + public string DownloadUrl { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("digest")] + public string Digest { get; set; } + } + + private sealed class ComponentMetadata + { + public string ComponentId { get; set; } + public string ActiveVersion { get; set; } + public string PreviousVersion { get; set; } + public string Repository { get; set; } + public bool AutoUpdate { get; set; } = true; + public DateTimeOffset? LastUpdateCheckUtc { get; set; } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptComponentModels.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentModels.cs new file mode 100644 index 000000000..c13df2885 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptComponentModels.cs @@ -0,0 +1,76 @@ +#nullable enable +using System; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller.Service +{ + public enum DnsCryptComponentStage + { + CheckingRelease, + DownloadingArchive, + DownloadingSignature, + Verifying, + Installing, + Prepared, + ValidatingRuntime, + Activating, + Starting, + Removing, + Ready, + } + + public sealed record DnsCryptComponentProgress( + DnsCryptComponentStage Stage, + long BytesTransferred = 0, + long? TotalBytes = null); + + public sealed record DnsCryptReleaseInfo( + Version Version, + string TagName, + string ArchiveName, + Uri ArchiveDownloadUri, + string SignatureName, + Uri SignatureDownloadUri, + long ArchiveSize, + string Sha256Digest); + + public sealed record DnsCryptPreparedComponent( + Version Version, + string DirectoryPath, + string ExecutablePath); + + public sealed record DnsCryptComponentStatus( + bool IsInstalled, + Version? ActiveVersion, + Version? PreviousVersion, + string? ExecutablePath, + bool AutoUpdate, + DateTimeOffset? LastUpdateCheckUtc); + + public sealed record DnsPrivacySelfTestResult( + bool Passed, + bool RuntimeHealthy, + bool AdminInterceptionActive, + bool PlaintextFallbackBlocked, + bool PlaintextBootstrapDisabled, + bool SystemDnsIgnored, + bool AutomaticResolverPinned, + bool RouteThroughShadowsocks, + string Upstream, + string Transport, + System.Collections.Generic.IReadOnlyList Failures); + + public sealed record DnsCryptManagementStatus( + DnsPolicyMode Mode, + DnsCryptComponentStatus Component, + DnsCryptRuntimeStatus Runtime, + Version? LatestVersion, + string? LastMaintenanceError = null, + bool AutomaticResolvers = true, + System.Collections.Generic.IReadOnlyList? ActiveResolvers = null) + { + public bool UpdateAvailable => Component?.ActiveVersion is not null + && LatestVersion is not null + && LatestVersion > Component.ActiveVersion; + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptCoordinator.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptCoordinator.cs new file mode 100644 index 000000000..ba57baedd --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptCoordinator.cs @@ -0,0 +1,228 @@ +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Serializes DNSCrypt management transactions that span component, runtime, configuration, + /// and capture state. Individual managers keep their own internal locks; this gate protects + /// the higher-level transaction from interleaving with another user or background operation. + /// The lifecycle token additionally cancels queued work during suspend/application shutdown. + /// + internal sealed class DnsCryptCoordinator : IDisposable + { + private readonly SemaphoreSlim _managementGate = new(1, 1); + private readonly object _stateLock = new(); + private CancellationTokenSource _lifecycleCancellation = new(); + private string? _currentOperation; + private CancellationTokenSource? _currentCancellation; + private bool _acceptingOperations = true; + private bool _disposed; + + public string? CurrentOperation + { + get + { + lock (_stateLock) + { + return _currentOperation; + } + } + } + + public bool IsBusy => CurrentOperation is not null; + + public bool IsSuspended + { + get + { + lock (_stateLock) + { + return !_acceptingOperations; + } + } + } + + public async Task ExecuteExclusiveAsync( + string operation, + Func> action, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(operation); + ArgumentNullException.ThrowIfNull(action); + + CancellationToken lifecycleToken; + lock (_stateLock) + { + if (!_acceptingOperations) + throw new InvalidOperationException("DNSCrypt coordinator is suspended."); + lifecycleToken = _lifecycleCancellation.Token; + } + + using var operationCancellation = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + lifecycleToken); + bool gateHeld = false; + try + { + await _managementGate.WaitAsync(operationCancellation.Token).ConfigureAwait(false); + gateHeld = true; + operationCancellation.Token.ThrowIfCancellationRequested(); + + lock (_stateLock) + { + if (!_acceptingOperations || lifecycleToken != _lifecycleCancellation.Token) + throw new OperationCanceledException("DNSCrypt coordinator lifecycle changed.", operationCancellation.Token); + + _currentOperation = operation; + _currentCancellation = operationCancellation; + } + + return await action(operationCancellation.Token).ConfigureAwait(false); + } + finally + { + lock (_stateLock) + { + if (ReferenceEquals(_currentCancellation, operationCancellation)) + { + _currentOperation = null; + _currentCancellation = null; + } + } + + if (gateHeld) + _managementGate.Release(); + } + } + + public void CancelCurrentOperation() + { + CancellationTokenSource? cancellation; + lock (_stateLock) + { + cancellation = _currentCancellation; + } + + TryCancel(cancellation); + } + + /// + /// Atomically stops accepting DNS management work, cancels active and queued transactions, + /// waits for the gate, then executes teardown while the coordinator remains suspended. + /// Call from the next controller Start() lifecycle. + /// + public async Task SuspendAndExecuteAsync( + string operation, + Func action) + { + ArgumentException.ThrowIfNullOrWhiteSpace(operation); + ArgumentNullException.ThrowIfNull(action); + + CancellationTokenSource lifecycleCancellation; + CancellationTokenSource? currentCancellation; + lock (_stateLock) + { + _acceptingOperations = false; + lifecycleCancellation = _lifecycleCancellation; + currentCancellation = _currentCancellation; + } + + TryCancel(lifecycleCancellation); + TryCancel(currentCancellation); + + await _managementGate.WaitAsync(CancellationToken.None).ConfigureAwait(false); + try + { + lock (_stateLock) + { + _currentOperation = operation; + _currentCancellation = null; + } + + await action(CancellationToken.None).ConfigureAwait(false); + } + finally + { + lock (_stateLock) + { + _currentOperation = null; + _currentCancellation = null; + } + _managementGate.Release(); + } + } + + public void Resume() + { + CancellationTokenSource? oldCancellation = null; + lock (_stateLock) + { + if (_acceptingOperations) + return; + + oldCancellation = _lifecycleCancellation; + _lifecycleCancellation = new CancellationTokenSource(); + _acceptingOperations = true; + } + + oldCancellation.Dispose(); + } + + public Task ExecuteExclusiveAsync( + string operation, + Func action, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(action); + return ExecuteExclusiveAsync( + operation, + async token => + { + await action(token).ConfigureAwait(false); + return null; + }, + cancellationToken); + } + + public void Dispose() + { + CancellationTokenSource lifecycleCancellation; + CancellationTokenSource? currentCancellation; + lock (_stateLock) + { + if (_disposed) + { + return; + } + + _disposed = true; + _acceptingOperations = false; + lifecycleCancellation = _lifecycleCancellation; + currentCancellation = _currentCancellation; + _currentCancellation = null; + _currentOperation = null; + } + + TryCancel(lifecycleCancellation); + TryCancel(currentCancellation); + lifecycleCancellation.Dispose(); + _managementGate.Dispose(); + GC.SuppressFinalize(this); + } + + private static void TryCancel(CancellationTokenSource? cancellation) + { + try + { + cancellation?.Cancel(); + } + catch (ObjectDisposedException) + { + // The transaction completed between observing and cancelling its token source. + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptCountrySelector.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptCountrySelector.cs new file mode 100644 index 000000000..6674430f8 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptCountrySelector.cs @@ -0,0 +1,166 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller.Service +{ + internal static class DnsCryptCountrySelector + { + public static async Task> SelectAsync( + IReadOnlyList resolvers, + IReadOnlyList targetCountries, + DnsCryptConfig config, + Func> countryLookup, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resolvers); + ArgumentNullException.ThrowIfNull(targetCountries); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(countryLookup); + + DnsCryptResolverInfo[] compatible = OrderCompatibleResolvers(resolvers, config).ToArray(); + var selected = new List(); + + foreach (IpCountryInfo target in targetCountries + .Where(item => !string.IsNullOrWhiteSpace(item.CountryCode)) + .GroupBy(item => item.CountryCode, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First())) + { + cancellationToken.ThrowIfCancellationRequested(); + + // CountryCode on the resolver catalog is populated exclusively by GeoIP + // from the resolver endpoint IP. Never use a resolver name, city token, + // description, provider name, or other textual metadata as geography. + DnsCryptResolverInfo? match = compatible.FirstOrDefault(resolver => + !selected.Contains(resolver.Name, StringComparer.OrdinalIgnoreCase) + && string.Equals(resolver.CountryCode, target.CountryCode, StringComparison.OrdinalIgnoreCase)); + + if (match is null) + { + // Catalog enrichment is best-effort. If GeoIP was temporarily unavailable + // while the list was loaded, retry unresolved resolver endpoints here. + foreach (DnsCryptResolverInfo resolver in compatible.Where(resolver => + string.IsNullOrWhiteSpace(resolver.CountryCode) + && !selected.Contains(resolver.Name, StringComparer.OrdinalIgnoreCase))) + { + cancellationToken.ThrowIfCancellationRequested(); + IpCountryInfo? located = await ResolveResolverCountryByEndpointAsync( + resolver, countryLookup, cancellationToken).ConfigureAwait(false); + if (located is null + || !string.Equals(located.CountryCode, target.CountryCode, StringComparison.OrdinalIgnoreCase)) + continue; + + match = resolver; + break; + } + } + + if (match is not null) + selected.Add(match.Name); + } + + return selected; + } + + internal static bool IsCompatible(DnsCryptResolverInfo resolver, DnsCryptConfig config) + { + if (resolver is null || string.IsNullOrWhiteSpace(resolver.Name)) + return false; + if (!IsSupportedProtocol(resolver.Protocol)) + return false; + if (resolver.IPv6 && !config.ipv6Servers) + return false; + if (!resolver.IPv6 && !config.ipv4Servers) + return false; + if (config.requireDnssec && resolver.Dnssec != true) + return false; + if (config.requireNoLog && !resolver.NoLog) + return false; + if (config.requireNoFilter && !resolver.NoFilter) + return false; + return true; + } + + private static bool IsDnsCryptProtocol(string? protocol) + => !string.IsNullOrWhiteSpace(protocol) && protocol.Contains("DNSCrypt", StringComparison.OrdinalIgnoreCase); + + private static bool IsSupportedProtocol(string? protocol) + => IsDnsCryptProtocol(protocol) + || string.Equals(protocol?.Trim(), "DoH", StringComparison.OrdinalIgnoreCase); + + internal static IReadOnlyList SelectFallback( + IReadOnlyList resolvers, + DnsCryptConfig config, + int maxCount = 1) + { + ArgumentNullException.ThrowIfNull(resolvers); + ArgumentNullException.ThrowIfNull(config); + ArgumentOutOfRangeException.ThrowIfLessThan(maxCount, 1); + + return OrderCompatibleResolvers(resolvers, config) + .Select(item => item.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(maxCount) + .ToArray(); + } + + private static IEnumerable OrderCompatibleResolvers( + IReadOnlyList resolvers, + DnsCryptConfig config) + { + IEnumerable compatible = resolvers.Where(item => IsCompatible(item, config)); + return config.routeThroughShadowsocks + ? compatible + .OrderBy(item => IsDnsCryptProtocol(item.Protocol) ? 0 : 1) + .ThenBy(item => item.LatencyMs ?? int.MaxValue) + .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) + : compatible + .OrderBy(item => item.LatencyMs ?? int.MaxValue) + .ThenBy(item => IsDnsCryptProtocol(item.Protocol) ? 0 : 1) + .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase); + } + + internal static async Task ResolveResolverCountryByEndpointAsync( + DnsCryptResolverInfo resolver, + Func> countryLookup, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(resolver); + ArgumentNullException.ThrowIfNull(countryLookup); + + string[] allHosts = (resolver.Addresses ?? Array.Empty()) + .Where(address => !string.IsNullOrWhiteSpace(address)) + .Select(IpCountryService.NormalizeHost) + .Where(host => !string.IsNullOrWhiteSpace(host)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + string[] literalHosts = allHosts + .Where(host => System.Net.IPAddress.TryParse(host, out _)) + .ToArray(); + string[] hosts = literalHosts.Length > 0 ? literalHosts : allHosts; + + var located = new List(); + foreach (string host in hosts) + { + cancellationToken.ThrowIfCancellationRequested(); + IpCountryInfo? country = await countryLookup(host, cancellationToken).ConfigureAwait(false); + if (country is not null) + located.Add(country); + } + + string[] countryCodes = located + .Select(country => country.CountryCode) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + return countryCodes.Length == 1 + ? located.First(country => string.Equals( + country.CountryCode, countryCodes[0], StringComparison.OrdinalIgnoreCase)) + : null; + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptMaintenancePolicy.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptMaintenancePolicy.cs new file mode 100644 index 000000000..aefb6ed6e --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptMaintenancePolicy.cs @@ -0,0 +1,77 @@ +#nullable enable +using System; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller.Service +{ + internal static class DnsCryptMaintenancePolicy + { + internal static readonly TimeSpan MinimumAutomaticCheckInterval = TimeSpan.FromHours(24); + internal static readonly TimeSpan StartupDelay = TimeSpan.FromSeconds(30); + internal static readonly TimeSpan IdleRecheckInterval = TimeSpan.FromMinutes(15); + internal static readonly TimeSpan ClockSkewTolerance = TimeSpan.FromMinutes(10); + + internal static bool IsAutomaticUpdateEnabled( + DnsCryptComponentStatus? component, + DnsCryptConfig? config) + { + return component?.IsInstalled == true && config?.autoUpdate == true; + } + + internal static bool IsAutomaticCheckDue( + DnsCryptComponentStatus? component, + DnsCryptConfig? config, + DateTimeOffset nowUtc) + { + if (!IsAutomaticUpdateEnabled(component, config)) + return false; + + DateTimeOffset? lastCheckUtc = component!.LastUpdateCheckUtc; + if (!lastCheckUtc.HasValue) + return true; + + DateTimeOffset normalizedNow = nowUtc.ToUniversalTime(); + DateTimeOffset normalizedLast = lastCheckUtc.Value.ToUniversalTime(); + if (normalizedLast > normalizedNow + ClockSkewTolerance) + { + // A persisted timestamp far in the future indicates that the wall clock was + // corrected backwards. Allow one check now; RecordUpdateCheck() will persist + // the corrected timestamp before any network request is made. + return true; + } + if (normalizedLast > normalizedNow) + return false; + + return normalizedNow - normalizedLast >= MinimumAutomaticCheckInterval; + } + + internal static TimeSpan GetDelayUntilNextAutomaticCheck( + DnsCryptComponentStatus? component, + DnsCryptConfig? config, + DateTimeOffset nowUtc) + { + if (!IsAutomaticUpdateEnabled(component, config)) + return IdleRecheckInterval; + + DateTimeOffset? lastCheckUtc = component!.LastUpdateCheckUtc; + if (!lastCheckUtc.HasValue) + return TimeSpan.Zero; + + DateTimeOffset normalizedNow = nowUtc.ToUniversalTime(); + DateTimeOffset normalizedLast = lastCheckUtc.Value.ToUniversalTime(); + if (normalizedLast > normalizedNow + ClockSkewTolerance) + return TimeSpan.Zero; + if (normalizedLast > normalizedNow) + normalizedLast = normalizedNow; + + TimeSpan remaining = MinimumAutomaticCheckInterval - (normalizedNow - normalizedLast); + if (remaining <= TimeSpan.Zero) + return TimeSpan.Zero; + + // Re-evaluate local settings periodically so toggling Auto Update does not require + // restarting the application, while GitHub itself is still contacted at most once + // per 24-hour metadata interval. + return remaining < IdleRecheckInterval ? remaining : IdleRecheckInterval; + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptPresentation.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptPresentation.cs new file mode 100644 index 000000000..409897347 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptPresentation.cs @@ -0,0 +1,31 @@ +#nullable enable +using System; + +namespace Shadowsocks.Controller.Service +{ + public sealed record DnsCryptTrayPresentation(string LocalizationKey, Version? Version = null); + + public static class DnsCryptPresentation + { + public static DnsCryptTrayPresentation GetTrayPresentation(DnsCryptManagementStatus status) + { + ArgumentNullException.ThrowIfNull(status); + if (!status.Component.IsInstalled) + return new DnsCryptTrayPresentation("DNSCrypt: Not installed"); + + DnsCryptRuntimeState runtimeState = status.Runtime.State; + if (runtimeState == DnsCryptRuntimeState.Updating) + return new DnsCryptTrayPresentation("DNSCrypt: Updating…"); + if (runtimeState == DnsCryptRuntimeState.Starting) + return new DnsCryptTrayPresentation("DNSCrypt: Starting…"); + if (runtimeState == DnsCryptRuntimeState.Failed) + return new DnsCryptTrayPresentation("DNSCrypt: Failed"); + if (status.UpdateAvailable) + return new DnsCryptTrayPresentation("DNSCrypt: Update available · {0}", status.LatestVersion); + + return runtimeState == DnsCryptRuntimeState.Running + ? new DnsCryptTrayPresentation("DNSCrypt: Running · {0}", status.Component.ActiveVersion) + : new DnsCryptTrayPresentation("DNSCrypt: Stopped · {0}", status.Component.ActiveVersion); + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptResolverCatalogBootstrapper.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptResolverCatalogBootstrapper.cs new file mode 100644 index 000000000..3254f15f1 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptResolverCatalogBootstrapper.cs @@ -0,0 +1,590 @@ +#nullable enable +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NLog; + +namespace Shadowsocks.Controller.Service +{ + internal interface IDnsCryptResolverCatalogBootstrapper + { + Task EnsureFreshAsync( + string cacheDirectory, + string shadowsocksSocks5Host, + int shadowsocksSocks5Port, + CancellationToken cancellationToken); + } + + internal sealed class DnsCryptResolverCatalogBootstrapper : IDnsCryptResolverCatalogBootstrapper + { + internal const string ResolverListFileName = "public-resolvers.md"; + internal const string ResolverListSignatureFileName = ResolverListFileName + ".minisig"; + + private const int MaxCatalogBytes = 8 * 1024 * 1024; + private const int MaxSignatureBytes = 64 * 1024; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly TimeSpan Freshness = TimeSpan.FromHours(24); + private static readonly Uri[] CatalogUris = + [ + new("https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md"), + new("https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md"), + new("https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/public-resolvers.md"), + ]; + + private readonly Func>? testDownloader; + private readonly Func signatureVerifier; + + public DnsCryptResolverCatalogBootstrapper() + : this(null, MinisignVerifier.VerifyFileAllowLegacy) + { + } + + internal DnsCryptResolverCatalogBootstrapper( + Func>? testDownloader, + Func? signatureVerifier = null) + { + this.testDownloader = testDownloader; + this.signatureVerifier = signatureVerifier ?? MinisignVerifier.VerifyFileAllowLegacy; + } + + public async Task EnsureFreshAsync( + string cacheDirectory, + string shadowsocksSocks5Host, + int shadowsocksSocks5Port, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory); + ArgumentException.ThrowIfNullOrWhiteSpace(shadowsocksSocks5Host); + ArgumentOutOfRangeException.ThrowIfLessThan(shadowsocksSocks5Port, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(shadowsocksSocks5Port, IPEndPoint.MaxPort); + + string directory = Path.GetFullPath(cacheDirectory); + Directory.CreateDirectory(directory); + string catalogPath = Path.Combine(directory, ResolverListFileName); + string signaturePath = Path.Combine(directory, ResolverListSignatureFileName); + + bool existingValid = IsValidCatalogPair(catalogPath, signaturePath); + if (existingValid && DateTime.UtcNow - File.GetLastWriteTimeUtc(catalogPath) < Freshness) + return; + + var failures = new List(); + try + { + if (testDownloader is not null) + { + await DownloadAndPublishAsync( + testDownloader, catalogPath, signaturePath, cancellationToken).ConfigureAwait(false); + } + else + { + using var resolver = new ShadowsocksDohResolver(shadowsocksSocks5Host, shadowsocksSocks5Port); + using HttpClient client = ShadowsocksDohHttpClient.CreateCatalogClient( + shadowsocksSocks5Host, + shadowsocksSocks5Port, + resolver); + + async Task DownloadAsync(Uri uri, CancellationToken token) + => await DownloadBoundedAsync( + client, + uri, + uri.AbsolutePath.EndsWith(".minisig", StringComparison.OrdinalIgnoreCase) + ? MaxSignatureBytes + : MaxCatalogBytes, + token).ConfigureAwait(false); + + await DownloadAndPublishAsync( + DownloadAsync, catalogPath, signaturePath, cancellationToken).ConfigureAwait(false); + } + return; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + failures.Add(exception); + } + + // A stale cache is still cryptographically authenticated and is preferable to + // plaintext/system-DNS fallback. Keep it available and retry refresh later. + if (existingValid) + { + Logger.Warn(failures[0], "DNSCrypt signed resolver catalog refresh failed; using the existing verified cache."); + return; + } + + throw new DnsCryptBootstrapException( + "DNSCrypt could not refresh the signed resolver catalog through Cloudflare/Google DoH over Shadowsocks.", + failures[0]); + } + + private async Task DownloadAndPublishAsync( + Func> downloader, + string catalogPath, + string signaturePath, + CancellationToken cancellationToken) + { + Exception? lastFailure = null; + foreach (Uri catalogUri in CatalogUris) + { + cancellationToken.ThrowIfCancellationRequested(); + string tempCatalog = catalogPath + $".{Guid.NewGuid():N}.new"; + string tempSignature = signaturePath + $".{Guid.NewGuid():N}.new"; + try + { + byte[] catalog = await downloader(catalogUri, cancellationToken).ConfigureAwait(false); + byte[] signature = await downloader(new Uri(catalogUri.AbsoluteUri + ".minisig"), cancellationToken).ConfigureAwait(false); + if (catalog.Length is 0 or > MaxCatalogBytes) + throw new InvalidDataException("DNSCrypt resolver catalog has an invalid size."); + if (signature.Length is 0 or > MaxSignatureBytes) + throw new InvalidDataException("DNSCrypt resolver catalog signature has an invalid size."); + + await File.WriteAllBytesAsync(tempCatalog, catalog, cancellationToken).ConfigureAwait(false); + await File.WriteAllBytesAsync(tempSignature, signature, cancellationToken).ConfigureAwait(false); + string signatureText = Encoding.UTF8.GetString(signature); + if (!signatureVerifier(tempCatalog, signatureText, DnsCryptTomlGenerator.PublicResolversMinisignKey)) + throw new InvalidDataException("DNSCrypt resolver catalog Minisign verification failed."); + + File.Move(tempCatalog, catalogPath, overwrite: true); + File.Move(tempSignature, signaturePath, overwrite: true); + DateTime now = DateTime.UtcNow; + File.SetLastWriteTimeUtc(catalogPath, now); + File.SetLastWriteTimeUtc(signaturePath, now); + Logger.Info("DNSCrypt signed resolver catalog refreshed through Shadowsocks: {0}", catalogUri); + return; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + lastFailure = exception; + Logger.Debug(exception, "DNSCrypt resolver catalog source failed through DoH-over-Shadowsocks: {0}", catalogUri); + } + finally + { + TryDelete(tempCatalog); + TryDelete(tempSignature); + } + } + + throw new DnsCryptBootstrapException( + "All DNSCrypt resolver catalog mirrors failed through DoH-over-Shadowsocks.", + lastFailure); + } + + private bool IsValidCatalogPair(string catalogPath, string signaturePath) + { + if (!File.Exists(catalogPath) || !File.Exists(signaturePath)) + return false; + try + { + string signature = File.ReadAllText(signaturePath, Encoding.UTF8); + return signatureVerifier(catalogPath, signature, DnsCryptTomlGenerator.PublicResolversMinisignKey); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException or FormatException) + { + Logger.Debug(exception, "Existing DNSCrypt resolver catalog cache pair is invalid."); + return false; + } + } + + private static async Task DownloadBoundedAsync( + HttpClient client, + Uri uri, + int maxBytes, + CancellationToken cancellationToken) + { + using HttpRequestMessage request = new(HttpMethod.Get, uri); + using HttpResponseMessage response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + if (response.Content.Headers.ContentLength is long length && length > maxBytes) + throw new InvalidDataException($"Response from {uri.Host} exceeds the allowed size."); + await using Stream source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await ReadBoundedAsync(source, maxBytes, cancellationToken).ConfigureAwait(false); + } + + internal static async Task ReadBoundedAsync(Stream source, int maxBytes, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxBytes); + + using var output = new MemoryStream(Math.Min(maxBytes, 64 * 1024)); + byte[] buffer = new byte[32 * 1024]; + while (true) + { + int read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + if (output.Length + read > maxBytes) + throw new InvalidDataException("Response exceeds the allowed size."); + output.Write(buffer.AsSpan(0, read)); + } + return output.ToArray(); + } + + private static void TryDelete(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch + { + } + } + } + + internal sealed class ShadowsocksDohResolver : IDisposable + { + private const ushort TypeA = 1; + private const ushort TypeAaaa = 28; + private const int MaxDnsMessageBytes = 64 * 1024; + private static readonly ushort[] QueryTypes = [TypeA, TypeAaaa]; + private static readonly DohProvider[] Providers = + [ + new("Cloudflare", new Uri("https://cloudflare-dns.com/dns-query")), + new("Google", new Uri("https://dns.google/dns-query")), + ]; + + private readonly HttpClient client; + private readonly Func>? queryOverride; + + internal ShadowsocksDohResolver( + string proxyHost, + int proxyPort, + Func>? queryOverride = null) + { + this.queryOverride = queryOverride; + client = ShadowsocksDohHttpClient.CreateProviderClient(proxyHost, proxyPort); + } + + internal async Task ResolveAsync(string host, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(host); + if (IPAddress.TryParse(host.Trim('[', ']'), out IPAddress? literal)) + return [literal]; + + Exception? lastFailure = null; + foreach (ushort type in QueryTypes) + { + foreach (DohProvider provider in Providers) + { + cancellationToken.ThrowIfCancellationRequested(); + ushort transactionId = RandomTransactionId(); + byte[] query = BuildQuery(host, type, transactionId); + try + { + byte[] response = queryOverride is null + ? await QueryProviderAsync(provider, query, cancellationToken).ConfigureAwait(false) + : await queryOverride(provider, query, cancellationToken).ConfigureAwait(false); + IPAddress[] addresses = ParseAddresses(response, transactionId, type); + if (addresses.Length > 0) + return addresses; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + lastFailure = exception; + } + } + } + + throw new DnsCryptBootstrapException( + $"Cloudflare and Google DoH could not resolve '{host}' through Shadowsocks.", + lastFailure); + } + + private async Task QueryProviderAsync( + DohProvider provider, + byte[] query, + CancellationToken cancellationToken) + { + using HttpRequestMessage request = new(HttpMethod.Post, provider.Endpoint); + using var content = new ByteArrayContent(query); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/dns-message"); + request.Content = content; + request.Headers.Accept.ParseAdd("application/dns-message"); + using HttpResponseMessage response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await DnsCryptResolverCatalogBootstrapper.ReadBoundedAsync( + stream, + MaxDnsMessageBytes, + cancellationToken).ConfigureAwait(false); + } + + internal static byte[] BuildQuery(string host, ushort queryType, ushort transactionId) + { + string normalized = host.Trim().TrimEnd('.'); + if (string.IsNullOrWhiteSpace(normalized)) + throw new ArgumentException("A DNS hostname is required.", nameof(host)); + + using var output = new MemoryStream(256); + Span header = stackalloc byte[12]; + header.Clear(); + BinaryPrimitives.WriteUInt16BigEndian(header, transactionId); + BinaryPrimitives.WriteUInt16BigEndian(header[2..], 0x0100); + BinaryPrimitives.WriteUInt16BigEndian(header[4..], 1); + output.Write(header); + foreach (string label in normalized.Split('.')) + { + byte[] bytes = Encoding.ASCII.GetBytes(label); + if (bytes.Length is 0 or > 63) + throw new ArgumentException("DNS hostname contains an invalid label.", nameof(host)); + output.WriteByte((byte)bytes.Length); + output.Write(bytes); + } + output.WriteByte(0); + Span tail = stackalloc byte[4]; + BinaryPrimitives.WriteUInt16BigEndian(tail, queryType); + BinaryPrimitives.WriteUInt16BigEndian(tail[2..], 1); + output.Write(tail); + return output.ToArray(); + } + + internal static IPAddress[] ParseAddresses(byte[] response, ushort expectedTransactionId, ushort queryType) + { + ArgumentNullException.ThrowIfNull(response); + ReadOnlySpan data = response; + if (data.Length < 12) + throw new InvalidDataException("DoH returned a truncated DNS response."); + if (BinaryPrimitives.ReadUInt16BigEndian(data) != expectedTransactionId) + throw new InvalidDataException("DoH returned a DNS response with a mismatched transaction ID."); + ushort flags = BinaryPrimitives.ReadUInt16BigEndian(data[2..]); + if ((flags & 0x8000) == 0 || (flags & 0x000F) != 0) + throw new InvalidDataException("DoH returned an unsuccessful DNS response."); + + int questionCount = BinaryPrimitives.ReadUInt16BigEndian(data[4..]); + int answerCount = BinaryPrimitives.ReadUInt16BigEndian(data[6..]); + int offset = 12; + for (int i = 0; i < questionCount; i++) + { + offset = SkipName(data, offset); + EnsureAvailable(data, offset, 4); + offset += 4; + } + + var addresses = new List(); + for (int i = 0; i < answerCount; i++) + { + offset = SkipName(data, offset); + EnsureAvailable(data, offset, 10); + ushort type = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); + ushort dnsClass = BinaryPrimitives.ReadUInt16BigEndian(data[(offset + 2)..]); + ushort dataLength = BinaryPrimitives.ReadUInt16BigEndian(data[(offset + 8)..]); + offset += 10; + EnsureAvailable(data, offset, dataLength); + if (dnsClass == 1 && type == queryType + && ((type == TypeA && dataLength == 4) || (type == TypeAaaa && dataLength == 16))) + { + addresses.Add(new IPAddress(data.Slice(offset, dataLength))); + } + offset += dataLength; + } + return addresses.Distinct().ToArray(); + } + + private static int SkipName(ReadOnlySpan data, int offset) + { + while (true) + { + EnsureAvailable(data, offset, 1); + byte length = data[offset++]; + if (length == 0) + return offset; + if ((length & 0xC0) == 0xC0) + { + EnsureAvailable(data, offset, 1); + return offset + 1; + } + if ((length & 0xC0) != 0 || length > 63) + throw new InvalidDataException("DoH returned an invalid DNS name."); + EnsureAvailable(data, offset, length); + offset += length; + } + } + + private static void EnsureAvailable(ReadOnlySpan data, int offset, int count) + { + if (offset < 0 || count < 0 || offset > data.Length - count) + throw new InvalidDataException("DoH returned a truncated DNS response."); + } + + private static ushort RandomTransactionId() + { + Span bytes = stackalloc byte[2]; + RandomNumberGenerator.Fill(bytes); + return BinaryPrimitives.ReadUInt16BigEndian(bytes); + } + + public void Dispose() => client.Dispose(); + + internal sealed record DohProvider(string Name, Uri Endpoint); + } + + internal static class ShadowsocksDohHttpClient + { + internal static HttpClient CreateProviderClient(string proxyHost, int proxyPort) + { + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + ConnectTimeout = TimeSpan.FromSeconds(10), + ConnectCallback = async (context, cancellationToken) => + { + string host = context.DnsEndPoint.Host; + string destinationIp = string.Equals(host, "cloudflare-dns.com", StringComparison.OrdinalIgnoreCase) + ? "1.1.1.1" + : string.Equals(host, "dns.google", StringComparison.OrdinalIgnoreCase) + ? "8.8.8.8" + : throw new InvalidOperationException($"Unexpected DoH provider host '{host}'."); + Socket socket = await Socks5Connector.ConnectAsync( + proxyHost, + proxyPort, + destinationIp, + context.DnsEndPoint.Port, + cancellationToken).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + }, + }; + return new HttpClient(handler, disposeHandler: true) + { + Timeout = TimeSpan.FromSeconds(15), + }; + } + + internal static HttpClient CreateCatalogClient( + string proxyHost, + int proxyPort, + ShadowsocksDohResolver resolver) + { + ArgumentNullException.ThrowIfNull(resolver); + return CreateTunneledClient( + () => proxyHost, + () => proxyPort, + TimeSpan.FromSeconds(30), + allowAutoRedirect: false, + (host, _, cancellationToken) => resolver.ResolveAsync(host, cancellationToken)); + } + + internal static HttpClient CreateTunneledClient( + Func proxyHostProvider, + Func proxyPortProvider, + TimeSpan timeout, + bool allowAutoRedirect = true) + { + ArgumentNullException.ThrowIfNull(proxyHostProvider); + ArgumentNullException.ThrowIfNull(proxyPortProvider); + return CreateTunneledClient( + proxyHostProvider, + proxyPortProvider, + timeout, + allowAutoRedirect, + static async (host, endpoint, cancellationToken) => + { + using var resolver = new ShadowsocksDohResolver(endpoint.ProxyHost, endpoint.ProxyPort); + return await resolver.ResolveAsync(host, cancellationToken).ConfigureAwait(false); + }); + } + + private static HttpClient CreateTunneledClient( + Func proxyHostProvider, + Func proxyPortProvider, + TimeSpan timeout, + bool allowAutoRedirect, + Func> resolver) + { + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = allowAutoRedirect, + ConnectTimeout = TimeSpan.FromSeconds(10), + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + ConnectCallback = async (context, cancellationToken) => + { + string proxyHost = proxyHostProvider(); + int proxyPort = proxyPortProvider(); + ArgumentException.ThrowIfNullOrWhiteSpace(proxyHost); + if (proxyPort is < 1 or > IPEndPoint.MaxPort) + throw new DnsCryptComponentNetworkException( + $"The Shadowsocks SOCKS5 port '{proxyPort}' is not valid for DNSCrypt upstream access."); + + var endpoint = new ProxyEndpoint(proxyHost, proxyPort); + IPAddress[] addresses = await resolver( + context.DnsEndPoint.Host, + endpoint, + cancellationToken).ConfigureAwait(false); + Exception? lastFailure = null; + foreach (IPAddress address in addresses) + { + try + { + Socket socket = await Socks5Connector.ConnectAsync( + proxyHost, + proxyPort, + address.ToString(), + context.DnsEndPoint.Port, + cancellationToken).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + lastFailure = exception; + } + } + + throw new HttpRequestException( + $"No DoH-resolved address for '{context.DnsEndPoint.Host}' was reachable through Shadowsocks.", + lastFailure); + }, + }; + return new HttpClient(handler, disposeHandler: true) + { + Timeout = timeout, + }; + } + + private readonly record struct ProxyEndpoint(string ProxyHost, int ProxyPort); + } + + internal sealed class NoOpDnsCryptResolverCatalogBootstrapper : IDnsCryptResolverCatalogBootstrapper + { + internal static NoOpDnsCryptResolverCatalogBootstrapper Instance { get; } = new(); + + private NoOpDnsCryptResolverCatalogBootstrapper() + { + } + + public Task EnsureFreshAsync( + string cacheDirectory, + string shadowsocksSocks5Host, + int shadowsocksSocks5Port, + CancellationToken cancellationToken) + => Task.CompletedTask; + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptResolverLatencyProbe.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptResolverLatencyProbe.cs new file mode 100644 index 000000000..7b48fb045 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptResolverLatencyProbe.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Shadowsocks.Controller.Service +{ + internal static class DnsCryptResolverLatencyProbe + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(1400); + private const int DefaultConcurrency = 24; + + public static async Task> ProbeAsync( + IReadOnlyList resolvers, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resolvers); + using var gate = new SemaphoreSlim(DefaultConcurrency, DefaultConcurrency); + var results = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + Task[] tasks = resolvers + .Where(resolver => resolver is not null && !string.IsNullOrWhiteSpace(resolver.Name)) + .GroupBy(resolver => resolver.Name, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .Select(resolver => ProbeOneAsync(resolver, gate, results, cancellationToken)) + .ToArray(); + await Task.WhenAll(tasks).ConfigureAwait(false); + return results.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + } + + private static async Task ProbeOneAsync( + DnsCryptResolverInfo resolver, + SemaphoreSlim gate, + ConcurrentDictionary results, + CancellationToken cancellationToken) + { + if (!TryGetEndpoint(resolver, out IPEndPoint endpoint)) + return; + + await gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + using var socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(DefaultTimeout); + long started = Stopwatch.GetTimestamp(); + try + { + await socket.ConnectAsync(endpoint, timeout.Token).ConfigureAwait(false); + int elapsed = Math.Max(1, checked((int)Stopwatch.GetElapsedTime(started).TotalMilliseconds)); + results[resolver.Name] = elapsed; + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + } + catch (SocketException) + { + } + } + finally + { + gate.Release(); + } + } + + internal static bool TryGetEndpoint(DnsCryptResolverInfo resolver, out IPEndPoint endpoint) + { + endpoint = null; + if (resolver is null) + return false; + + IPAddress address = null; + foreach (string candidate in resolver.Addresses ?? Array.Empty()) + { + string host = IpCountryService.NormalizeHost(candidate); + if (IPAddress.TryParse(host, out IPAddress parsed) && IpCountryService.IsPublicAddress(parsed)) + { + address = parsed; + break; + } + } + if (address is null) + return false; + + int port = resolver.Ports?.FirstOrDefault(value => value is >= 1 and <= 65535) ?? 0; + if (port == 0) + port = 443; + endpoint = new IPEndPoint(address, port); + return true; + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.Dns.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.Dns.cs new file mode 100644 index 000000000..215459476 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.Dns.cs @@ -0,0 +1,610 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Shadowsocks.Controller.Service +{ + public sealed partial class DnsCryptRuntimeManager + { + internal static Version ParseReportedVersion(string output) + { + if (string.IsNullOrWhiteSpace(output)) + return null; + foreach (string line in output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) + { + string value = line.Trim(); + if (value.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + value = value.Substring(1); + if (Version.TryParse(value, out Version parsed)) + return parsed; + } + return null; + } + + internal static int AllocateLoopbackPort() + { + SocketException lastError = null; + + // Windows maintains dynamic/excluded port state separately for TCP and UDP. + // Ask UDP for an ephemeral port first because dnscrypt-proxy must serve UDP DNS, + // then verify that the same numeric port can also be bound by TCP. + for (int attempt = 0; attempt < 32; attempt++) + { + if (TryReserveLoopbackPortPair(0, out int ephemeralPort, out lastError)) + return ephemeralPort; + } + + // Hyper-V/container hosts can reserve large transport-specific chunks of the + // normal dynamic range. Fall back to random application ports below the + // default Windows dynamic range, but still prove both transports can bind. + for (int attempt = 0; attempt < 64; attempt++) + { + int candidate = RandomNumberGenerator.GetInt32(10000, 49152); + if (TryReserveLoopbackPortPair(candidate, out int fallbackPort, out lastError)) + return fallbackPort; + } + + throw new InvalidOperationException( + "Unable to allocate a TCP/UDP loopback port for DNSCrypt Proxy.", + lastError); + } + + private static bool TryReserveLoopbackPortPair( + int requestedPort, + out int port, + out SocketException error) + { + port = 0; + error = null; + UdpClient udp = null; + TcpListener tcp = null; + try + { + udp = new UdpClient(AddressFamily.InterNetwork); + udp.Client.ExclusiveAddressUse = true; + udp.Client.Bind(new IPEndPoint(IPAddress.Loopback, requestedPort)); + port = ((IPEndPoint)udp.Client.LocalEndPoint).Port; + + tcp = new TcpListener(IPAddress.Loopback, port); + tcp.Server.ExclusiveAddressUse = true; + tcp.Start(); + return true; + } + catch (SocketException ex) + { + error = ex; + port = 0; + return false; + } + finally + { + try { tcp?.Stop(); } catch { } + udp?.Dispose(); + } + } + + internal static async Task DnsHealthCheckAsync( + int port, + TimeSpan timeout, + CancellationToken cancellationToken) + { + if (port is < 1 or > 65535) + return false; + ushort transactionId = (ushort)RandomNumberGenerator.GetInt32(0, 65536); + byte[] query = BuildDnsAQuery(transactionId, "example.com"); + using var client = new UdpClient(AddressFamily.InterNetwork); + try + { + client.Connect(new IPEndPoint(IPAddress.Loopback, port)); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(timeout); + await client.SendAsync(query, query.Length).ConfigureAwait(false); + UdpReceiveResult response = await client.ReceiveAsync(timeoutCts.Token).ConfigureAwait(false); + return IsValidDnsResponse(response.Buffer, transactionId, "example.com"); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + return false; + } + catch (SocketException) + { + return false; + } + } + + public async Task> ResolveHostAsync( + string hostName, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(hostName)) + throw new ArgumentException("A DNS host name is required.", nameof(hostName)); + if (IPAddress.TryParse(hostName.Trim(), out IPAddress parsed)) + return [parsed]; + + DnsCryptRuntimeStatus current = GetStatus(); + if (!current.IsServing || current.Port is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt Proxy is not serving DNS requests."); + + bool queryIpv4; + bool queryIpv6; + lock (stateLock) + { + queryIpv4 = lastStartOptions?.Config?.ipv4Servers != false; + queryIpv6 = lastStartOptions?.Config?.ipv6Servers == true; + } + + var addresses = new List(); + if (queryIpv4) + addresses.AddRange(await QueryAddressesAsync(current.Port, hostName.Trim(), 1, cancellationToken).ConfigureAwait(false)); + if (queryIpv6) + addresses.AddRange(await QueryAddressesAsync(current.Port, hostName.Trim(), 28, cancellationToken).ConfigureAwait(false)); + + IPAddress[] distinct = addresses.Distinct().ToArray(); + if (distinct.Length == 0) + throw new SocketException((int)SocketError.HostNotFound); + return distinct; + } + + private static async Task> QueryAddressesAsync( + int port, + string hostName, + ushort queryType, + CancellationToken cancellationToken) + { + ushort transactionId = (ushort)RandomNumberGenerator.GetInt32(0, 65536); + byte[] query = BuildDnsQuery(transactionId, hostName, queryType); + using var client = new UdpClient(AddressFamily.InterNetwork); + client.Connect(new IPEndPoint(IPAddress.Loopback, port)); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(4)); + await client.SendAsync(query, query.Length).ConfigureAwait(false); + UdpReceiveResult response = await client.ReceiveAsync(timeoutCts.Token).ConfigureAwait(false); + return ParseAddressResponse(response.Buffer, transactionId, queryType); + } + + internal static IReadOnlyList ParseAddressResponse( + byte[] response, + ushort transactionId, + ushort queryType) + { + if (response is null || response.Length < 12) + return []; + if (((response[0] << 8) | response[1]) != transactionId) + return []; + ushort flags = (ushort)((response[2] << 8) | response[3]); + if ((flags & 0x8000) == 0 || (flags & 0x000F) != 0) + return []; + + int questionCount = (response[4] << 8) | response[5]; + int answerCount = (response[6] << 8) | response[7]; + int offset = 12; + for (int i = 0; i < questionCount; i++) + { + if (!SkipDnsName(response, ref offset) || offset + 4 > response.Length) + return []; + offset += 4; + } + + var addresses = new List(); + for (int i = 0; i < answerCount; i++) + { + if (!SkipDnsName(response, ref offset) || offset + 10 > response.Length) + return addresses; + ushort type = (ushort)((response[offset] << 8) | response[offset + 1]); + ushort dnsClass = (ushort)((response[offset + 2] << 8) | response[offset + 3]); + ushort dataLength = (ushort)((response[offset + 8] << 8) | response[offset + 9]); + offset += 10; + if (offset + dataLength > response.Length) + return addresses; + + if (dnsClass == 1 && type == queryType) + { + if (type == 1 && dataLength == 4) + addresses.Add(new IPAddress(response.AsSpan(offset, 4))); + else if (type == 28 && dataLength == 16) + addresses.Add(new IPAddress(response.AsSpan(offset, 16))); + } + offset += dataLength; + } + return addresses; + } + + private static bool SkipDnsName(byte[] message, ref int offset) + { + int labels = 0; + while (offset < message.Length && labels++ < 128) + { + byte length = message[offset++]; + if (length == 0) + return true; + if ((length & 0xC0) == 0xC0) + { + if (offset >= message.Length) + return false; + offset++; + return true; + } + if ((length & 0xC0) != 0 || length > 63 || offset + length > message.Length) + return false; + offset += length; + } + return false; + } + + internal static byte[] BuildDnsAQuery(ushort transactionId, string hostName) + => BuildDnsQuery(transactionId, hostName, 1); + + private static byte[] BuildDnsQuery(ushort transactionId, string hostName, ushort queryType) + { + if (string.IsNullOrWhiteSpace(hostName)) + throw new ArgumentException("A DNS host name is required.", nameof(hostName)); + if (queryType is not 1 and not 28) + throw new ArgumentOutOfRangeException(nameof(queryType)); + string[] labels = hostName.TrimEnd('.').Split('.'); + using var stream = new MemoryStream(64); + WriteUInt16NetworkOrder(stream, transactionId); + WriteUInt16NetworkOrder(stream, 0x0100); + WriteUInt16NetworkOrder(stream, 1); + WriteUInt16NetworkOrder(stream, 0); + WriteUInt16NetworkOrder(stream, 0); + WriteUInt16NetworkOrder(stream, 0); + foreach (string label in labels) + { + byte[] bytes = Encoding.ASCII.GetBytes(label); + if (bytes.Length is < 1 or > 63) + throw new ArgumentException("DNS host name contains an invalid label.", nameof(hostName)); + stream.WriteByte((byte)bytes.Length); + stream.Write(bytes, 0, bytes.Length); + } + stream.WriteByte(0); + WriteUInt16NetworkOrder(stream, queryType); + WriteUInt16NetworkOrder(stream, 1); + return stream.ToArray(); + } + + internal static bool IsValidDnsResponse(byte[] response, ushort transactionId, string expectedHostName = "example.com") + { + if (response is null || response.Length < 17) + return false; + ushort responseId = (ushort)((response[0] << 8) | response[1]); + if (responseId != transactionId) + return false; + ushort flags = (ushort)((response[2] << 8) | response[3]); + bool isResponse = (flags & 0x8000) != 0; + int responseCode = flags & 0x000F; + ushort questionCount = (ushort)((response[4] << 8) | response[5]); + if (!isResponse || questionCount < 1 || responseCode != 0) + return false; + + if (string.IsNullOrWhiteSpace(expectedHostName)) + return false; + + int offset = 12; + var questionName = new StringBuilder(); + while (true) + { + if (offset >= response.Length) + return false; + int labelLength = response[offset++]; + if (labelLength == 0) + break; + // A compressed QNAME in the echoed question is legal DNS, but DNSCrypt + // health-check responses to our simple query should echo the literal name. + // Reject compression here to keep validation deterministic and spoof-resistant. + if ((labelLength & 0xC0) != 0 || labelLength > 63 || offset + labelLength > response.Length) + return false; + if (questionName.Length > 0) + questionName.Append('.'); + for (int index = 0; index < labelLength; index++) + { + byte value = response[offset + index]; + if (value is < 0x21 or > 0x7E) + return false; + questionName.Append((char)value); + } + offset += labelLength; + } + + if (!string.Equals(questionName.ToString(), expectedHostName.TrimEnd('.'), StringComparison.OrdinalIgnoreCase)) + return false; + if (offset + 4 > response.Length) + return false; + ushort queryType = (ushort)((response[offset] << 8) | response[offset + 1]); + ushort queryClass = (ushort)((response[offset + 2] << 8) | response[offset + 3]); + return queryType == 1 && queryClass == 1; + } + + private static void WriteUInt16NetworkOrder(MemoryStream stream, ushort value) + { + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)(value & 0xFF)); + } + + internal static IReadOnlyDictionary ParseResolverLatencies(string output) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string line in SplitLines(output)) + { + int okEnd = line.IndexOf("] OK", StringComparison.OrdinalIgnoreCase); + if (okEnd <= 0) + continue; + int nameStart = line.LastIndexOf('[', okEnd); + if (nameStart < 0 || nameStart + 1 >= okEnd) + continue; + int rttMarker = line.IndexOf("rtt:", okEnd, StringComparison.OrdinalIgnoreCase); + if (rttMarker < 0) + continue; + int digitStart = rttMarker + 4; + while (digitStart < line.Length && char.IsWhiteSpace(line[digitStart])) + digitStart++; + int digitEnd = digitStart; + while (digitEnd < line.Length && char.IsDigit(line[digitEnd])) + digitEnd++; + if (digitEnd == digitStart || !int.TryParse(line[digitStart..digitEnd], out int latencyMs)) + continue; + string name = line[(nameStart + 1)..okEnd].Trim(); + if (name.Length == 0) + continue; + if (!result.TryGetValue(name, out int current) || latencyMs < current) + result[name] = latencyMs; + } + return result; + } + + internal static string ParseSelectedResolverName(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return string.Empty; + + const string marker = "Server with the lowest initial latency:"; + int markerIndex = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (markerIndex < 0) + return string.Empty; + + int start = markerIndex + marker.Length; + while (start < line.Length && char.IsWhiteSpace(line[start])) + start++; + int end = line.IndexOf(' ', start); + int comma = line.IndexOf(',', start); + if (end < 0 || (comma >= 0 && comma < end)) + end = comma; + if (end < 0) + end = line.Length; + return line[start..end].Trim(); + } + + private string BuildStartupTimeoutMessage() + { + string error; + lock (stateLock) + error = lastRuntimeUpstreamError; + string baseMessage = $"DNSCrypt Proxy did not pass the DNS health-check within {startupTimeout.TotalSeconds:0.###} seconds."; + return string.IsNullOrWhiteSpace(error) + ? baseMessage + : $"{baseMessage} Last dnscrypt-proxy error: {error}"; + } + + private void RecordRuntimeResolverMetrics(string line, string level) + { + bool notify = false; + string[] configuredNames; + lock (stateLock) + { + configuredNames = lastStartOptions?.Config?.serverNames? + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? Array.Empty(); + } + + foreach ((string name, int latencyMs) in ParseResolverLatencies(line)) + { + bool changed = !resolverLatencyCache.TryGetValue(name, out int previousLatency) || previousLatency != latencyMs; + resolverLatencyCache[name] = latencyMs; + if (changed && configuredNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + notify = true; + } + + string selected = ParseSelectedResolverName(line); + if (!string.IsNullOrWhiteSpace(selected)) + { + lock (stateLock) + { + if (!string.Equals(selectedRuntimeResolverName, selected, StringComparison.OrdinalIgnoreCase)) + { + selectedRuntimeResolverName = selected; + notify = true; + } + } + } + + if (level is "FATAL" or "CRITICAL" or "ERROR") + { + string value = line?.Trim() ?? string.Empty; + if (value.Length > 700) + value = value[..700]; + lock (stateLock) + lastRuntimeUpstreamError = value; + } + + if (notify) + ResolverMetricsChanged?.Invoke(this, EventArgs.Empty); + } + + private bool IsDiagnosticLoggingEnabled() + { + try + { + return diagnosticLoggingEnabled?.Invoke() != false; + } + catch + { + return false; + } + } + + private bool IsVerboseDiagnosticLoggingEnabled() + { + if (!IsDiagnosticLoggingEnabled()) + return false; + + try + { + return verboseLoggingEnabled?.Invoke() != false; + } + catch + { + return false; + } + } + + internal static bool IsResolverProbeDiagnostic(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return false; + + string[] markers = ["[NOTICE]", "[INFO]", "[WARNING]", "[WARN]", "[DEBUG]"]; + foreach (string marker in markers) + { + int markerIndex = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (markerIndex < 0) + continue; + + string payload = line[(markerIndex + marker.Length)..].TrimStart(); + if (payload.StartsWith('[')) + return true; + } + + return line.Contains("rtt:", StringComparison.OrdinalIgnoreCase) + || line.Contains("additional certificate", StringComparison.OrdinalIgnoreCase) + || line.Contains("post-quantum", StringComparison.OrdinalIgnoreCase) + || line.Contains(" TIMEOUT", StringComparison.OrdinalIgnoreCase); + } + + private void LogCommandOutput(string phase, DnsCryptCommandResult result) + { + if (!IsDiagnosticLoggingEnabled()) + return; + + bool verboseDiagnostics = IsVerboseDiagnosticLoggingEnabled(); + if (verboseDiagnostics) + { + foreach (string line in SplitLines(result.StandardOutput)) + Logger.Info($"DNSCryptProxy | {phase} | {line}"); + } + + foreach (string line in SplitLines(result.StandardError)) + { + string level = DetectRuntimeLogLevel(line, error: true); + string message = $"DNSCryptProxy | {phase} | {line}"; + if (level is "FATAL" or "CRITICAL" or "ERROR") + Logger.Error(message); + else if (verboseDiagnostics) + Logger.Warn(message); + } + } + + private void LogSettingsValidationLine(bool error, string line, Action captureError) + { + if (string.IsNullOrWhiteSpace(line)) + return; + + string level = DetectRuntimeLogLevel(line, error); + foreach ((string name, int latencyMs) in ParseResolverLatencies(line)) + resolverLatencyCache[name] = latencyMs; + if (level is "FATAL" or "CRITICAL" or "ERROR") + { + string value = line.Trim(); + if (value.Length > 700) + value = value[..700]; + captureError?.Invoke(value); + } + + string message = $"DNSCryptProxy | SETTINGS-VALIDATE | {line}"; + bool resolverProbe = IsResolverProbeDiagnostic(line); + bool dnsLogging = IsDiagnosticLoggingEnabled(); + if (!dnsLogging) + return; + bool verboseDiagnostics = resolverProbe && IsVerboseDiagnosticLoggingEnabled(); + if (level is "FATAL" or "CRITICAL" or "ERROR") + Logger.Error(message); + else if (level is "WARN" or "STDERR") + { + if (resolverProbe ? verboseDiagnostics : dnsLogging) + Logger.Warn(message); + } + else if (level == "DEBUG") + { + if (IsVerboseDiagnosticLoggingEnabled()) + Logger.Debug(message); + } + else if (resolverProbe ? verboseDiagnostics : dnsLogging) + Logger.Info(message); + } + + private void LogRuntimeLine(bool error, string line) + { + if (string.IsNullOrWhiteSpace(line)) + return; + + string level = DetectRuntimeLogLevel(line, error); + RecordRuntimeResolverMetrics(line, level); + bool dnsLogging = IsDiagnosticLoggingEnabled(); + if (!dnsLogging) + return; + + string message = $"DNSCryptProxy | {level} | {line}"; + bool resolverProbe = IsResolverProbeDiagnostic(line); + bool verboseDiagnostics = resolverProbe && IsVerboseDiagnosticLoggingEnabled(); + switch (level) + { + case "FATAL": + Logger.Fatal(message); + break; + case "CRITICAL": + case "ERROR": + Logger.Error(message); + break; + case "WARN": + case "STDERR": + if (resolverProbe ? verboseDiagnostics : dnsLogging) + Logger.Warn(message); + break; + case "DEBUG": + if (IsVerboseDiagnosticLoggingEnabled()) + Logger.Debug(message); + break; + default: + if (resolverProbe ? verboseDiagnostics : dnsLogging) + Logger.Info(message); + break; + } + } + + private static string DetectRuntimeLogLevel(string line, bool error) + { + if (line.Contains("[FATAL]", StringComparison.OrdinalIgnoreCase)) return "FATAL"; + if (line.Contains("[CRITICAL]", StringComparison.OrdinalIgnoreCase)) return "CRITICAL"; + if (line.Contains("[ERROR]", StringComparison.OrdinalIgnoreCase)) return "ERROR"; + if (line.Contains("[WARNING]", StringComparison.OrdinalIgnoreCase) + || line.Contains("[WARN]", StringComparison.OrdinalIgnoreCase)) return "WARN"; + if (line.Contains("[DEBUG]", StringComparison.OrdinalIgnoreCase)) return "DEBUG"; + if (line.Contains("[NOTICE]", StringComparison.OrdinalIgnoreCase)) return "NOTICE"; + if (line.Contains("[INFO]", StringComparison.OrdinalIgnoreCase)) return "INFO"; + return error ? "STDERR" : "INFO"; + } + + private static IEnumerable SplitLines(string value) => + string.IsNullOrWhiteSpace(value) + ? [] + : value.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Select(line => line.Trim()); + + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.cs new file mode 100644 index 000000000..50bba2a1a --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeManager.cs @@ -0,0 +1,1349 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Service +{ + public sealed partial class DnsCryptRuntimeManager : IDisposable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly TimeSpan VersionCheckTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan ConfigurationCheckTimeout = TimeSpan.FromSeconds(15); + private static readonly TimeSpan ResolverListTimeout = TimeSpan.FromSeconds(45); + private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan StopTimeout = TimeSpan.FromSeconds(3); + private static readonly JsonSerializerOptions s_resolverJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + private static readonly TimeSpan[] DefaultRestartDelays = + [ + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(10), + ]; + private readonly Func componentStatusProvider; + private readonly DnsCryptComponentManager ownedComponentManager; + private readonly string runtimeDirectory; + private readonly IDnsCryptRuntimePlatform platform; + private readonly Func portAllocator; + private readonly Func> healthChecker; + private readonly Func diagnosticLoggingEnabled; + private readonly Func verboseLoggingEnabled; + private readonly IDnsCryptResolverCatalogBootstrapper resolverCatalogBootstrapper; + private readonly TimeSpan[] restartDelays; + private readonly TimeSpan startupTimeout; + private readonly SemaphoreSlim lifecycleLock = new(1, 1); + private readonly object stateLock = new(); + private readonly ConcurrentDictionary resolverLatencyCache = new(StringComparer.OrdinalIgnoreCase); + + private IDnsCryptRunningProcess process; + private DnsCryptRuntimeStartOptions lastStartOptions; + private DnsCryptRuntimeStatus status; + private string lastRuntimeUpstreamError; + private string selectedRuntimeResolverName; + private CancellationTokenSource recoveryCancellation = new(); + private volatile bool stopRequested = true; + private bool disposed; + + public DnsCryptRuntimeManager( + DnsCryptComponentManager componentManager = null, + Func diagnosticLoggingEnabled = null, + Func verboseLoggingEnabled = null) + { + if (componentManager is null) + { + ownedComponentManager = new DnsCryptComponentManager(); + componentManager = ownedComponentManager; + } + + componentStatusProvider = componentManager.GetStatus; + runtimeDirectory = Path.GetFullPath(AppStoragePaths.DnsCryptRuntimeDirectory); + platform = new SystemDnsCryptRuntimePlatform(); + portAllocator = AllocateLoopbackPort; + healthChecker = DnsHealthCheckAsync; + this.diagnosticLoggingEnabled = diagnosticLoggingEnabled ?? (() => true); + this.verboseLoggingEnabled = verboseLoggingEnabled ?? (() => true); + resolverCatalogBootstrapper = new DnsCryptResolverCatalogBootstrapper(); + restartDelays = DefaultRestartDelays; + startupTimeout = StartupTimeout; + CleanupTransientRuntimeDirectories(); + status = CreateStoppedStatus(); + } + + internal DnsCryptRuntimeManager( + Func componentStatusProvider, + string runtimeDirectory, + IDnsCryptRuntimePlatform platform, + Func portAllocator, + Func> healthChecker, + IReadOnlyList restartDelays = null, + TimeSpan? startupTimeout = null, + Func diagnosticLoggingEnabled = null, + Func verboseLoggingEnabled = null, + IDnsCryptResolverCatalogBootstrapper resolverCatalogBootstrapper = null) + { + ArgumentNullException.ThrowIfNull(componentStatusProvider); + ArgumentNullException.ThrowIfNull(platform); + ArgumentNullException.ThrowIfNull(portAllocator); + ArgumentNullException.ThrowIfNull(healthChecker); + + this.componentStatusProvider = componentStatusProvider; + if (string.IsNullOrWhiteSpace(runtimeDirectory)) + throw new ArgumentException("A DNSCrypt runtime directory is required.", nameof(runtimeDirectory)); + this.runtimeDirectory = Path.GetFullPath(runtimeDirectory); + this.platform = platform; + this.portAllocator = portAllocator; + this.healthChecker = healthChecker; + this.diagnosticLoggingEnabled = diagnosticLoggingEnabled ?? (() => true); + this.verboseLoggingEnabled = verboseLoggingEnabled ?? (() => true); + this.resolverCatalogBootstrapper = resolverCatalogBootstrapper ?? NoOpDnsCryptResolverCatalogBootstrapper.Instance; + this.restartDelays = (restartDelays ?? DefaultRestartDelays).ToArray(); + if (this.restartDelays.Length == 0 || this.restartDelays.Any(delay => delay < TimeSpan.Zero)) + throw new ArgumentException("DNSCrypt restart delays must contain at least one non-negative delay.", nameof(restartDelays)); + this.startupTimeout = startupTimeout ?? StartupTimeout; + if (this.startupTimeout <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(startupTimeout)); + CleanupTransientRuntimeDirectories(); + status = CreateStoppedStatus(); + } + + public event EventHandler StatusChanged; + public event EventHandler ResolverMetricsChanged; + + public DnsCryptRuntimeStatus GetStatus() + { + ThrowIfDisposed(); + lock (stateLock) + { + if (process is null && status.State is DnsCryptRuntimeState.Stopped or DnsCryptRuntimeState.NotInstalled) + status = CreateStoppedStatus(); + return status; + } + } + + public IReadOnlyList GetActiveResolverNames() + { + ThrowIfDisposed(); + lock (stateLock) + { + // Prefer the resolver dnscrypt-proxy actually selected. Both Automatic and + // Manual mode are constrained to explicit DNSCrypt/DoH names at runtime. + if (!string.IsNullOrWhiteSpace(selectedRuntimeResolverName)) + return new[] { selectedRuntimeResolverName }; + string[] configured = lastStartOptions?.Config?.serverNames? + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? Array.Empty(); + return configured; + } + } + + public IReadOnlyDictionary GetResolverLatencies() + { + ThrowIfDisposed(); + return resolverLatencyCache.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + } + + public string GetLastRuntimeUpstreamError() + { + ThrowIfDisposed(); + lock (stateLock) + return lastRuntimeUpstreamError ?? string.Empty; + } + + public async Task StartAsync( + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + DnsCryptRuntimeStartOptions snapshot = SnapshotOptions(options); + await lifecycleLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + stopRequested = false; + ResetRecoveryCancellation(); + await StopProcessCoreAsync(updateStatus: false, CancellationToken.None).ConfigureAwait(false); + try + { + lock (stateLock) + { + lastStartOptions = snapshot; + } + return await StartProcessCoreAsync(snapshot, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + stopRequested = true; + SetStatus(CreateStoppedStatus()); + throw; + } + catch (Exception exception) + { + stopRequested = true; + DnsCryptRuntimeStatus currentStatus = GetStatus(); + if (currentStatus.State != DnsCryptRuntimeState.NotInstalled) + { + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Failed, + TryGetInstalledVersion(), + 0, + 0, + null, + GetConfigPath(), + exception.Message)); + } + throw; + } + } + finally + { + lifecycleLock.Release(); + } + } + + public async Task RestartAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + DnsCryptRuntimeStartOptions options; + lock (stateLock) + { + options = lastStartOptions; + } + if (options is null) + throw new InvalidOperationException("DNSCrypt Proxy has not been started yet, so there is no runtime configuration to restart."); + return await StartAsync(options, cancellationToken).ConfigureAwait(false); + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + await lifecycleLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + stopRequested = true; + recoveryCancellation.Cancel(); + await StopProcessCoreAsync(updateStatus: true, cancellationToken).ConfigureAwait(false); + } + finally + { + lifecycleLock.Release(); + } + } + + /// + /// Marks component maintenance as Updating without stopping a healthy runtime. + /// + public DnsCryptRuntimeStatus BeginUpdate() + { + ThrowIfDisposed(); + DnsCryptRuntimeStatus previous = GetStatus(); + if (previous.State == DnsCryptRuntimeState.NotInstalled) + return previous; + + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Updating, + previous.Version, + previous.ProcessId, + previous.Port, + previous.StartedAtUtc, + previous.ConfigPath, + null)); + return previous; + } + + /// + /// Leaves Updating unless another runtime transition already replaced it. + /// + public void EndUpdate(DnsCryptRuntimeStatus previous, bool succeeded) + { + ThrowIfDisposed(); + DnsCryptRuntimeStatus current = GetStatus(); + if (current.State != DnsCryptRuntimeState.Updating) + return; + + IDnsCryptRunningProcess currentProcess; + lock (stateLock) + { + currentProcess = process; + } + + if (currentProcess is not null && !currentProcess.HasExited) + { + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Running, + TryGetInstalledVersion() ?? current.Version ?? previous?.Version, + currentProcess.Id, + current.Port, + current.StartedAtUtc, + current.ConfigPath ?? GetConfigPath(), + null)); + return; + } + + if (!succeeded && previous?.State == DnsCryptRuntimeState.Failed) + { + SetStatus(previous); + return; + } + + if (!succeeded && previous?.IsRunning == true) + { + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Failed, + TryGetInstalledVersion() ?? previous.Version, + 0, + 0, + null, + GetConfigPath(), + "DNSCrypt Proxy maintenance failed and the previous runtime could not be restored.")); + return; + } + + SetStatus(CreateStoppedStatus()); + } + + public async Task HealthCheckAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + DnsCryptRuntimeStatus current = GetStatus(); + if (current.State != DnsCryptRuntimeState.Running || current.Port is < 1 or > 65535) + return false; + return await healthChecker(current.Port, TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false); + } + + /// + /// Uses dnscrypt-proxy's signed resolver source in an isolated maintenance profile and + /// -list-all -json to obtain the current resolver catalog before active runtime startup. + /// dnscrypt-proxy receives no remote source URL and no plaintext/system-DNS bootstrap path. + /// + public async Task> ListResolversAsync( + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + DnsCryptComponentStatus componentStatus = componentStatusProvider(); + if (componentStatus is null || !componentStatus.IsInstalled || componentStatus.ActiveVersion is null + || string.IsNullOrWhiteSpace(componentStatus.ExecutablePath) || !File.Exists(componentStatus.ExecutablePath)) + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + + DnsCryptRuntimeStartOptions snapshot = SnapshotOptions(options); + await EnsureResolverCatalogCacheAsync(snapshot, cancellationToken).ConfigureAwait(false); + string listDirectory = Path.Combine(runtimeDirectory, $".list-{Guid.NewGuid():N}"); + Directory.CreateDirectory(listDirectory); + SeedResolverSourceCache(listDirectory); + try + { + int port = portAllocator(); + if (port is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt runtime port allocator returned an invalid port."); + + string configPath = Path.Combine(listDirectory, "dnscrypt-proxy.toml"); + string toml = DnsCryptTomlGenerator.Generate( + port, + snapshot.Config, + snapshot.ShadowsocksSocks5Port, + DnsCryptTomlPurpose.ResolverCatalog); + await File.WriteAllTextAsync(configPath, toml, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + + DnsCryptCommandResult result = await platform.ExecuteAsync( + componentStatus.ExecutablePath, + listDirectory, + ["-list-all", "-json", "-config", configPath], + ResolverListTimeout, + cancellationToken).ConfigureAwait(false); + if (result.ExitCode != 0) + { + LogCommandOutput("LIST", result); + throw new InvalidOperationException($"dnscrypt-proxy -list-all failed with exit code {result.ExitCode}: {result.StandardError?.Trim()}"); + } + + IReadOnlyDictionary commandLatencies = ParseResolverLatencies(result.StandardError); + foreach ((string name, int latencyMs) in commandLatencies) + resolverLatencyCache[name] = latencyMs; + DnsCryptResolverInfo[] resolvers = ParseResolverList(result.StandardOutput) + .Select(resolver => resolverLatencyCache.TryGetValue(resolver.Name, out int latencyMs) + ? resolver with { LatencyMs = latencyMs } + : resolver) + .ToArray(); + PersistResolverSourceCache(listDirectory); + if (IsDiagnosticLoggingEnabled()) + { + Logger.Info( + "DNSCryptProxy | LIST | Loaded {0} resolvers; RTT available for {1}.", + resolvers.Length, + resolvers.Count(resolver => resolver.LatencyMs.HasValue)); + } + return resolvers; + } + finally + { + try + { + if (Directory.Exists(listDirectory)) + Directory.Delete(listDirectory, recursive: true); + } + catch (Exception exception) + { + Logger.Warn(exception, "DNSCryptProxy | LIST | Failed to remove resolver-list runtime directory."); + } + } + } + + internal static IReadOnlyList ParseResolverList(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return Array.Empty(); + + try + { + List summaries = + JsonSerializer.Deserialize>(json, s_resolverJsonOptions) ?? []; + return summaries + .Where(item => !string.IsNullOrWhiteSpace(item.Name)) + .Select(item => new DnsCryptResolverInfo( + item.Name.Trim(), + item.Proto?.Trim() ?? string.Empty, + item.IPv6, + item.Dnssec, + item.NoLog, + item.NoFilter, + item.Description?.Trim() ?? string.Empty, + item.Addrs?.Where(address => !string.IsNullOrWhiteSpace(address)).Select(address => address.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray() ?? []) + { + Ports = item.Ports?.Where(port => port is >= 1 and <= 65535).Distinct().ToArray() ?? Array.Empty(), + Stamp = item.Stamp?.Trim() ?? string.Empty, + }) + .GroupBy(item => item.Name, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + catch (JsonException exception) + { + throw new InvalidDataException("dnscrypt-proxy returned an invalid resolver list JSON payload.", exception); + } + } + + /// + /// Validates a prepared, not-yet-active DNSCrypt version without changing component metadata + /// or replacing the currently running DNSCrypt instance. The candidate runs from an isolated + /// temporary runtime directory on its own loopback port and is always terminated afterwards. + /// + public async Task ValidatePreparedAsync( + DnsCryptPreparedComponent prepared, + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(prepared); + ArgumentNullException.ThrowIfNull(prepared.Version); + ArgumentException.ThrowIfNullOrWhiteSpace(prepared.ExecutablePath); + if (!File.Exists(prepared.ExecutablePath)) + throw new FileNotFoundException("Prepared DNSCrypt Proxy executable was not found.", prepared.ExecutablePath); + + DnsCryptRuntimeStartOptions snapshot = SnapshotOptions(options); + string validationDirectory = Path.Combine(runtimeDirectory, $".validate-{Guid.NewGuid():N}"); + Directory.CreateDirectory(validationDirectory); + IDnsCryptRunningProcess candidate = null; + try + { + // Verify the exact prepared binary before it is allowed to perform any catalog/network + // operation. A version mismatch must fail closed before resolver discovery. + DnsCryptCommandResult versionResult = await platform.ExecuteAsync( + prepared.ExecutablePath, validationDirectory, ["-version"], VersionCheckTimeout, cancellationToken).ConfigureAwait(false); + LogCommandOutput("VALIDATE-VERSION", versionResult); + if (versionResult.ExitCode != 0) + throw new InvalidOperationException($"dnscrypt-proxy -version failed with exit code {versionResult.ExitCode}."); + Version reportedVersion = ParseReportedVersion(versionResult.StandardOutput); + if (reportedVersion is null || !reportedVersion.Equals(prepared.Version)) + { + throw new InvalidDataException( + $"DNSCrypt executable version '{versionResult.StandardOutput?.Trim()}' does not match prepared component version {prepared.Version}."); + } + + snapshot = await ResolvePreparedValidationOptionsAsync( + prepared.ExecutablePath, validationDirectory, snapshot, cancellationToken).ConfigureAwait(false); + + int port = portAllocator(); + if (port is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt runtime port allocator returned an invalid port."); + + string configPath = Path.Combine(validationDirectory, "dnscrypt-proxy.toml"); + string toml = DnsCryptTomlGenerator.Generate( + port, + snapshot.Config, + snapshot.ShadowsocksSocks5Port, + staticResolverStamps: snapshot.StaticResolverStamps); + await File.WriteAllTextAsync(configPath, toml, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + + await CheckConfigurationAsync( + prepared.ExecutablePath, + validationDirectory, + configPath, + "VALIDATE-CHECK", + cancellationToken).ConfigureAwait(false); + + candidate = platform.Start( + prepared.ExecutablePath, validationDirectory, ["-config", configPath], + line => LogRuntimeLine(false, line), line => LogRuntimeLine(true, line)); + + using var startupCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + startupCts.CancelAfter(startupTimeout); + while (!startupCts.IsCancellationRequested && !candidate.HasExited) + { + if (await healthChecker(port, TimeSpan.FromSeconds(1), startupCts.Token).ConfigureAwait(false)) + { + if (IsDiagnosticLoggingEnabled()) + Logger.Info($"DNSCryptProxy | VALIDATE | Prepared version {prepared.Version} passed runtime DNS health-check on 127.0.0.1:{port}."); + return; + } + await Task.Delay(200, startupCts.Token).ConfigureAwait(false); + } + + if (candidate.HasExited) + throw new InvalidOperationException("Prepared DNSCrypt Proxy exited before the DNS health-check succeeded."); + throw new TimeoutException($"Prepared DNSCrypt Proxy did not pass the DNS health-check within {startupTimeout.TotalSeconds:0.###} seconds."); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException($"Prepared DNSCrypt Proxy did not pass the DNS health-check within {startupTimeout.TotalSeconds:0.###} seconds."); + } + finally + { + if (candidate is not null) + await TerminateProcessAsync(candidate, CancellationToken.None).ConfigureAwait(false); + try + { + if (Directory.Exists(validationDirectory)) + Directory.Delete(validationDirectory, recursive: true); + } + catch (Exception exception) + { + Logger.Warn(exception, "DNSCryptProxy | VALIDATE | Failed to remove validation runtime directory."); + } + } + } + + /// + /// Validates a DNSCrypt settings snapshot in an isolated candidate process while the + /// current runtime remains online. This prevents a bad manual resolver from tearing down + /// a healthy DNS path before reachability is known. + /// + public async Task ValidateSettingsAsync( + DnsCryptRuntimeStartOptions options, + TimeSpan timeout, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(timeout, TimeSpan.Zero); + + DnsCryptComponentStatus componentStatus = componentStatusProvider(); + if (componentStatus is null || !componentStatus.IsInstalled || componentStatus.ActiveVersion is null + || string.IsNullOrWhiteSpace(componentStatus.ExecutablePath) || !File.Exists(componentStatus.ExecutablePath)) + { + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + } + + DnsCryptRuntimeStartOptions snapshot = SnapshotOptions(options); + string validationDirectory = Path.Combine(runtimeDirectory, $".settings-{Guid.NewGuid():N}"); + Directory.CreateDirectory(validationDirectory); + IDnsCryptRunningProcess candidate = null; + string candidateError = string.Empty; + object candidateErrorLock = new(); + try + { + int port = portAllocator(); + if (port is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt runtime port allocator returned an invalid port."); + + string configPath = Path.Combine(validationDirectory, "dnscrypt-proxy.toml"); + string toml = DnsCryptTomlGenerator.Generate( + port, + snapshot.Config, + snapshot.ShadowsocksSocks5Port, + staticResolverStamps: snapshot.StaticResolverStamps); + await File.WriteAllTextAsync(configPath, toml, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + + candidate = platform.Start( + componentStatus.ExecutablePath, + validationDirectory, + ["-config", configPath], + line => LogSettingsValidationLine(false, line, value => + { + lock (candidateErrorLock) candidateError = value; + }), + line => LogSettingsValidationLine(true, line, value => + { + lock (candidateErrorLock) candidateError = value; + })); + + using var validationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + validationCts.CancelAfter(timeout); + while (!validationCts.IsCancellationRequested && !candidate.HasExited) + { + if (await healthChecker(port, TimeSpan.FromSeconds(1), validationCts.Token).ConfigureAwait(false)) + return; + await Task.Delay(150, validationCts.Token).ConfigureAwait(false); + } + + string lastError; + lock (candidateErrorLock) lastError = candidateError; + if (candidate.HasExited) + throw new InvalidOperationException(AppendDnsCryptUpstreamError( + "Selected DNSCrypt resolver exited before the DNS health-check succeeded.", lastError)); + throw new TimeoutException(AppendDnsCryptUpstreamError( + $"Selected DNSCrypt resolver did not become reachable within {timeout.TotalSeconds:0.###} seconds.", lastError)); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + string lastError; + lock (candidateErrorLock) lastError = candidateError; + throw new TimeoutException(AppendDnsCryptUpstreamError( + $"Selected DNSCrypt resolver did not become reachable within {timeout.TotalSeconds:0.###} seconds.", lastError)); + } + finally + { + if (candidate is not null) + await TerminateProcessAsync(candidate, CancellationToken.None).ConfigureAwait(false); + try + { + if (Directory.Exists(validationDirectory)) + Directory.Delete(validationDirectory, recursive: true); + } + catch (Exception exception) + { + Logger.Warn(exception, "DNSCryptProxy | SETTINGS-VALIDATE | Failed to remove validation runtime directory."); + } + } + } + + private static string AppendDnsCryptUpstreamError(string message, string upstreamError) + => string.IsNullOrWhiteSpace(upstreamError) ? message : $"{message} Last dnscrypt-proxy error: {upstreamError}"; + + private async Task StartProcessCoreAsync( + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken) + { + DnsCryptComponentStatus componentStatus = componentStatusProvider(); + if (componentStatus is null || !componentStatus.IsInstalled || componentStatus.ActiveVersion is null + || string.IsNullOrWhiteSpace(componentStatus.ExecutablePath) || !File.Exists(componentStatus.ExecutablePath)) + { + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.NotInstalled, + null, + 0, + 0, + null, + GetConfigPath(), + "DNSCrypt Proxy is not installed.")); + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + } + + Directory.CreateDirectory(runtimeDirectory); + int port = portAllocator(); + if (port is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt runtime port allocator returned an invalid port."); + + string configPath = GetConfigPath(); + string toml = DnsCryptTomlGenerator.Generate( + port, + options.Config, + options.ShadowsocksSocks5Port, + staticResolverStamps: options.StaticResolverStamps); + await File.WriteAllTextAsync(configPath, toml, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Starting, + componentStatus.ActiveVersion, + 0, + port, + null, + configPath, + null)); + + DnsCryptCommandResult versionResult = await platform.ExecuteAsync( + componentStatus.ExecutablePath, + runtimeDirectory, + ["-version"], + VersionCheckTimeout, + cancellationToken).ConfigureAwait(false); + LogCommandOutput("VERSION", versionResult); + if (versionResult.ExitCode != 0) + throw new InvalidOperationException($"dnscrypt-proxy -version failed with exit code {versionResult.ExitCode}."); + + Version reportedVersion = ParseReportedVersion(versionResult.StandardOutput); + if (reportedVersion is null || !reportedVersion.Equals(componentStatus.ActiveVersion)) + { + throw new InvalidDataException( + $"DNSCrypt executable version '{versionResult.StandardOutput?.Trim()}' does not match " + + $"installed component version {componentStatus.ActiveVersion}."); + } + + IDnsCryptRunningProcess startedProcess = null; + lock (stateLock) + { + lastRuntimeUpstreamError = null; + selectedRuntimeResolverName = null; + } + try + { + startedProcess = platform.Start( + componentStatus.ExecutablePath, + runtimeDirectory, + ["-config", configPath], + line => LogRuntimeLine(false, line), + line => LogRuntimeLine(true, line)); + + using var startupCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + startupCts.CancelAfter(startupTimeout); + bool healthy = false; + while (!startupCts.IsCancellationRequested && !startedProcess.HasExited) + { + if (await healthChecker(port, TimeSpan.FromSeconds(1), startupCts.Token).ConfigureAwait(false)) + { + healthy = true; + break; + } + await Task.Delay(200, startupCts.Token).ConfigureAwait(false); + } + + if (!healthy) + { + if (startedProcess.HasExited) + { + string upstreamError; + lock (stateLock) + upstreamError = lastRuntimeUpstreamError; + throw new DnsCryptRuntimeStartupException(AppendDnsCryptUpstreamError( + "DNSCrypt Proxy exited before the DNS health-check succeeded.", + upstreamError)); + } + throw new DnsCryptRuntimeStartupException(BuildStartupTimeoutMessage()); + } + + DnsCryptRuntimeStatus runningStatus = PromoteStartedProcessToRunning( + startedProcess, + componentStatus.ActiveVersion, + port, + configPath); + if (IsDiagnosticLoggingEnabled()) + Logger.Info($"DNSCryptProxy | RUNTIME | Started {componentStatus.ActiveVersion} on 127.0.0.1:{port} (PID {startedProcess.Id})."); + return runningStatus; + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + if (startedProcess is not null) + await TerminateProcessAsync(startedProcess, CancellationToken.None).ConfigureAwait(false); + throw new DnsCryptRuntimeStartupException(BuildStartupTimeoutMessage()); + } + catch + { + if (startedProcess is not null) + await TerminateProcessAsync(startedProcess, CancellationToken.None).ConfigureAwait(false); + throw; + } + } + + private async Task CheckConfigurationAsync( + string executablePath, + string workingDirectory, + string configPath, + string logPhase, + CancellationToken cancellationToken) + { + try + { + DnsCryptCommandResult result = await platform.ExecuteAsync( + executablePath, + workingDirectory, + ["-check", "-config", configPath], + ConfigurationCheckTimeout, + cancellationToken).ConfigureAwait(false); + LogCommandOutput(logPhase, result); + if (result.ExitCode != 0) + { + throw new InvalidDataException( + $"dnscrypt-proxy -check failed with exit code {result.ExitCode}: {result.StandardError?.Trim()}"); + } + } + catch (TimeoutException exception) + { + // dnscrypt-proxy -check may perform resolver certificate/network initialization. + // A slow/filtered network must not produce a false + // configuration failure: the real process startup + DNS probe below is the + // authoritative readiness check. Syntax/config errors still fail immediately + // because dnscrypt-proxy exits non-zero before this timeout. + if (IsDiagnosticLoggingEnabled()) + { + Logger.Warn(exception, + $"DNSCryptProxy | {logPhase} | Configuration preflight timed out; continuing with runtime health-check."); + } + } + } + + private DnsCryptRuntimeStatus PromoteStartedProcessToRunning( + IDnsCryptRunningProcess startedProcess, + Version version, + int port, + string configPath) + { + EventHandler handler; + DnsCryptRuntimeStatus runningStatus; + lock (stateLock) + { + if (startedProcess.HasExited) + throw new InvalidOperationException("DNSCrypt Proxy exited immediately after passing the DNS health-check."); + + process = startedProcess; + runningStatus = new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Running, + version, + startedProcess.Id, + port, + DateTimeOffset.UtcNow, + configPath, + null); + status = runningStatus; + handler = StatusChanged; + } + + // Publish Running before arming crash recovery. If the process exited in the + // tiny gap before event subscription, the HasExited check below converts that + // into the normal unexpected-exit path. This keeps status notifications ordered + // as Running -> Failed/Recovered instead of allowing a stale Running event after + // recovery has already started. + RaiseStatusChanged(handler, runningStatus); + startedProcess.Exited += OnProcessExited; + if (startedProcess.HasExited) + OnProcessExited(startedProcess, EventArgs.Empty); + return runningStatus; + } + + private async Task StopProcessCoreAsync(bool updateStatus, CancellationToken cancellationToken) + { + IDnsCryptRunningProcess current; + lock (stateLock) + { + current = process; + if (current is not null) + { + current.Exited -= OnProcessExited; + if (ReferenceEquals(process, current)) + process = null; + } + } + + if (current is not null) + { + await TerminateProcessAsync(current, cancellationToken).ConfigureAwait(false); + if (IsDiagnosticLoggingEnabled()) + Logger.Info("DNSCryptProxy | RUNTIME | Stopped."); + } + if (updateStatus) + SetStatus(CreateStoppedStatus()); + } + + private static async Task TerminateProcessAsync(IDnsCryptRunningProcess target, CancellationToken cancellationToken) + { + try + { + if (!target.HasExited) + target.Kill(); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(StopTimeout); + await target.WaitForExitAsync(timeout.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + } + catch (InvalidOperationException) + { + } + catch (Exception e) + { + Logger.Warn(e, "DNSCryptProxy | RUNTIME | Failed to terminate process cleanly."); + } + finally + { + target.Dispose(); + } + } + + private void OnProcessExited(object sender, EventArgs e) + { + if (sender is not IDnsCryptRunningProcess exitedProcess) + return; + exitedProcess.Exited -= OnProcessExited; + _ = RecoverFromUnexpectedExitAsync(exitedProcess); + } + + private async Task RecoverFromUnexpectedExitAsync(IDnsCryptRunningProcess exitedProcess) + { + DnsCryptRuntimeStartOptions options; + CancellationToken recoveryToken; + lock (stateLock) + { + if (disposed || stopRequested || !ReferenceEquals(process, exitedProcess)) + return; + process = null; + options = lastStartOptions; + recoveryToken = recoveryCancellation.Token; + } + exitedProcess.Dispose(); + + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Failed, + TryGetInstalledVersion(), + 0, + 0, + null, + GetConfigPath(), + "DNSCrypt Proxy exited unexpectedly.")); + Logger.Warn("DNSCryptProxy | RUNTIME | Process exited unexpectedly; starting recovery sequence."); + + for (int attempt = 0; attempt < restartDelays.Length; attempt++) + { + if (options is null || recoveryToken.IsCancellationRequested || stopRequested) + return; + + try + { + await Task.Delay(restartDelays[attempt], recoveryToken).ConfigureAwait(false); + await lifecycleLock.WaitAsync(recoveryToken).ConfigureAwait(false); + try + { + if (stopRequested || process is not null) + return; + Logger.Warn($"DNSCryptProxy | RUNTIME | Restart attempt {attempt + 1}/{restartDelays.Length}."); + await StartProcessCoreAsync(options, recoveryToken).ConfigureAwait(false); + return; + } + finally + { + lifecycleLock.Release(); + } + } + catch (OperationCanceledException) when (recoveryToken.IsCancellationRequested) + { + return; + } + catch (Exception exception) + { + Logger.Warn(exception, $"DNSCryptProxy | RUNTIME | Restart attempt {attempt + 1} failed."); + } + } + + SetStatus(new DnsCryptRuntimeStatus( + DnsCryptRuntimeState.Failed, + TryGetInstalledVersion(), + 0, + 0, + null, + GetConfigPath(), + $"DNSCrypt Proxy failed after {restartDelays.Length} automatic restart attempts.")); + } + + private void CleanupTransientRuntimeDirectories() + { + if (!Directory.Exists(runtimeDirectory)) + return; + + foreach (string directory in Directory.EnumerateDirectories(runtimeDirectory)) + { + string name = Path.GetFileName(directory); + if (!name.StartsWith(".validate-", StringComparison.OrdinalIgnoreCase) + && !name.StartsWith(".list-", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + Directory.Delete(directory, recursive: true); + } + catch (Exception exception) + { + Logger.Warn(exception, $"DNSCryptProxy | CLEANUP | Failed to remove stale runtime directory '{directory}'."); + } + } + } + + private DnsCryptRuntimeStatus CreateStoppedStatus() + { + DnsCryptComponentStatus component = null; + try { component = componentStatusProvider(); } catch { } + bool installed = component?.IsInstalled == true; + return new DnsCryptRuntimeStatus( + installed ? DnsCryptRuntimeState.Stopped : DnsCryptRuntimeState.NotInstalled, + installed ? component.ActiveVersion : null, + 0, + 0, + null, + GetConfigPath(), + null); + } + + private Version TryGetInstalledVersion() + { + try + { + DnsCryptComponentStatus component = componentStatusProvider(); + return component?.IsInstalled == true ? component.ActiveVersion : null; + } + catch + { + return null; + } + } + + private void SetStatus(DnsCryptRuntimeStatus newStatus) + { + EventHandler handler; + lock (stateLock) + { + status = newStatus; + handler = StatusChanged; + } + RaiseStatusChanged(handler, newStatus); + } + + private void RaiseStatusChanged( + EventHandler handler, + DnsCryptRuntimeStatus newStatus) + { + try + { + handler?.Invoke(this, new DnsCryptRuntimeStatusChangedEventArgs(newStatus)); + } + catch (Exception e) + { + Logger.Warn(e, "DNSCryptProxy | RUNTIME | StatusChanged subscriber failed."); + } + } + + private void ResetRecoveryCancellation() + { + CancellationTokenSource old = recoveryCancellation; + recoveryCancellation = new CancellationTokenSource(); + try { old.Cancel(); } catch { } + old.Dispose(); + } + + private async Task ResolvePreparedValidationOptionsAsync( + string executablePath, + string validationDirectory, + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken) + { + string[] requestedNames = DnsCryptTomlGenerator.NormalizeServerNames(options.Config.serverNames); + if (requestedNames.Length > 0 && HasStaticResolverStamps(options, requestedNames)) + return options; + + // Prepared-runtime validation is self-contained too: discover resolver stamps through + // the signed catalog in the isolated maintenance directory, then validate the exact + // selected resolver(s) as local [static.*] entries. The candidate runtime never + // performs a remote resolver-source refresh. + DnsCryptRuntimeStartOptions catalogOptions = SnapshotOptions(options); + catalogOptions.Config.serverNames = []; + catalogOptions.Config.routeThroughShadowsocks = false; + + int catalogPort = portAllocator(); + if (catalogPort is < 1 or > 65535) + throw new InvalidOperationException("DNSCrypt runtime port allocator returned an invalid port."); + + await EnsureResolverCatalogCacheAsync(options, cancellationToken).ConfigureAwait(false); + SeedResolverSourceCache(validationDirectory); + + string catalogConfigPath = Path.Combine(validationDirectory, "dnscrypt-proxy.catalog.toml"); + string catalogToml = DnsCryptTomlGenerator.Generate( + catalogPort, + catalogOptions.Config, + purpose: DnsCryptTomlPurpose.ResolverCatalog); + await File.WriteAllTextAsync( + catalogConfigPath, catalogToml, new UTF8Encoding(false), cancellationToken).ConfigureAwait(false); + + DnsCryptCommandResult listResult; + try + { + listResult = await platform.ExecuteAsync( + executablePath, + validationDirectory, + ["-list-all", "-json", "-config", catalogConfigPath], + ResolverListTimeout, + cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + throw new DnsCryptBootstrapException( + "Prepared DNSCrypt could not refresh or load the signed resolver catalog.", + exception); + } + if (listResult.ExitCode != 0) + { + LogCommandOutput("VALIDATE-LIST", listResult); + throw new DnsCryptBootstrapException( + $"Prepared dnscrypt-proxy -list-all failed with exit code {listResult.ExitCode}: {listResult.StandardError?.Trim()}"); + } + + IReadOnlyList resolvers; + try + { + resolvers = ParseResolverList(listResult.StandardOutput); + } + catch (InvalidDataException exception) + { + throw new DnsCryptBootstrapException( + "Prepared DNSCrypt returned an invalid signed resolver catalog.", + exception); + } + IReadOnlyList selected = requestedNames.Length > 0 + ? requestedNames + : DnsCryptCountrySelector.SelectFallback(resolvers, options.Config); + if (selected.Count == 0) + { + throw new InvalidOperationException( + "No DNSCrypt/DoH resolver in the signed catalog matches the selected automatic filters for prepared-runtime validation."); + } + + Dictionary stamps = BuildStaticResolverStampMap(resolvers, selected); + PersistResolverSourceCache(validationDirectory); + DnsCryptRuntimeStartOptions resolved = SnapshotOptions(options); + resolved.Config.serverNames = selected.ToList(); + return resolved with { StaticResolverStamps = stamps }; + } + + private static bool HasStaticResolverStamps( + DnsCryptRuntimeStartOptions options, + string[] serverNames) + { + if (options.StaticResolverStamps is null) + return false; + return serverNames.All(name => + options.StaticResolverStamps.TryGetValue(name, out string stamp) + && !string.IsNullOrWhiteSpace(stamp) + && stamp.StartsWith("sdns://", StringComparison.Ordinal)); + } + + internal static Dictionary BuildStaticResolverStampMap( + IReadOnlyList resolvers, + IReadOnlyList serverNames) + { + ArgumentNullException.ThrowIfNull(resolvers); + ArgumentNullException.ThrowIfNull(serverNames); + var byName = resolvers + .Where(resolver => resolver is not null && !string.IsNullOrWhiteSpace(resolver.Name)) + .GroupBy(resolver => resolver.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string serverName in serverNames) + { + if (!byName.TryGetValue(serverName, out DnsCryptResolverInfo resolver) + || string.IsNullOrWhiteSpace(resolver.Stamp) + || !resolver.Stamp.StartsWith("sdns://", StringComparison.Ordinal)) + { + throw new InvalidDataException( + $"The signed resolver catalog does not contain a usable stamp for '{serverName}'."); + } + result[serverName] = resolver.Stamp.Trim(); + } + return result; + } + + + private async Task EnsureResolverCatalogCacheAsync( + DnsCryptRuntimeStartOptions options, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(options); + if (ReferenceEquals(resolverCatalogBootstrapper, NoOpDnsCryptResolverCatalogBootstrapper.Instance)) + return; + if (!options.ShadowsocksSocks5Port.HasValue + || options.ShadowsocksSocks5Port.Value is < 1 or > IPEndPoint.MaxPort) + { + throw new DnsCryptBootstrapException( + "A working local Shadowsocks SOCKS5 endpoint is required to refresh the DNSCrypt resolver catalog through DoH."); + } + + string host = string.IsNullOrWhiteSpace(options.ShadowsocksSocks5Host) + ? "127.0.0.1" + : options.ShadowsocksSocks5Host; + await resolverCatalogBootstrapper.EnsureFreshAsync( + runtimeDirectory, + host, + options.ShadowsocksSocks5Port.Value, + cancellationToken).ConfigureAwait(false); + } + + private static DnsCryptRuntimeStartOptions SnapshotOptions(DnsCryptRuntimeStartOptions options) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(options.Config); + DnsCryptConfig source = options.Config; + var copy = new DnsCryptConfig + { + autoUpdate = source.autoUpdate, + requireDnssec = source.requireDnssec, + requireNoLog = source.requireNoLog, + requireNoFilter = source.requireNoFilter, + ipv4Servers = source.ipv4Servers, + ipv6Servers = source.ipv6Servers, + routeThroughShadowsocks = source.routeThroughShadowsocks, + automaticResolvers = source.automaticResolvers, + failClosed = source.failClosed, + serverNames = source.serverNames?.ToList() ?? [], + }; + return new DnsCryptRuntimeStartOptions(copy, options.ShadowsocksSocks5Port) + { + ShadowsocksSocks5Host = options.ShadowsocksSocks5Host, + StaticResolverStamps = options.StaticResolverStamps is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(options.StaticResolverStamps, StringComparer.OrdinalIgnoreCase), + }; + } + + private void PersistResolverSourceCache(string sourceDirectory) + { + if (string.IsNullOrWhiteSpace(sourceDirectory)) + return; + + try + { + // dnscrypt-proxy treats the resolver source as an authenticated pair. Persisting + // only the markdown file makes the offline catalog cache unusable; the catalog + // profile intentionally has no remote URLs or plaintext/system-DNS fallback. + PersistResolverSourceCacheFile(sourceDirectory, "public-resolvers.md"); + PersistResolverSourceCacheFile(sourceDirectory, "public-resolvers.md.minisig"); + } + catch (IOException exception) + { + Logger.Debug(exception, "DNSCryptProxy | LIST | Could not persist signed resolver cache pair for runtime reuse."); + } + catch (UnauthorizedAccessException exception) + { + Logger.Debug(exception, "DNSCryptProxy | LIST | Could not persist signed resolver cache pair for runtime reuse."); + } + } + + private void PersistResolverSourceCacheFile(string sourceDirectory, string fileName) + { + string source = Path.Combine(sourceDirectory, fileName); + if (!File.Exists(source)) + return; + + Directory.CreateDirectory(runtimeDirectory); + string destination = Path.Combine(runtimeDirectory, fileName); + string temporary = destination + $".{Guid.NewGuid():N}.new"; + try + { + File.Copy(source, temporary, overwrite: false); + File.Move(temporary, destination, overwrite: true); + File.SetLastWriteTimeUtc(destination, File.GetLastWriteTimeUtc(source)); + } + finally + { + try + { + if (File.Exists(temporary)) + File.Delete(temporary); + } + catch + { + } + } + } + + private void SeedResolverSourceCache(string destinationDirectory) + { + if (string.IsNullOrWhiteSpace(destinationDirectory)) + return; + + try + { + SeedResolverSourceCacheFile(destinationDirectory, "public-resolvers.md"); + SeedResolverSourceCacheFile(destinationDirectory, "public-resolvers.md.minisig"); + } + catch (IOException exception) + { + Logger.Debug(exception, "DNSCryptProxy | LIST | Could not seed signed resolver cache pair."); + } + catch (UnauthorizedAccessException exception) + { + Logger.Debug(exception, "DNSCryptProxy | LIST | Could not seed signed resolver cache pair."); + } + } + + private void SeedResolverSourceCacheFile(string destinationDirectory, string fileName) + { + string source = Path.Combine(runtimeDirectory, fileName); + if (!File.Exists(source)) + return; + + string destination = Path.Combine(destinationDirectory, fileName); + if (File.Exists(destination)) + return; + + File.Copy(source, destination, overwrite: false); + File.SetLastWriteTimeUtc(destination, File.GetLastWriteTimeUtc(source)); + } + + private string GetConfigPath() => Path.Combine(runtimeDirectory, "dnscrypt-proxy.toml"); + + public void Dispose() + { + if (disposed) + return; + disposed = true; + stopRequested = true; + try { recoveryCancellation.Cancel(); } catch { } + try + { + lifecycleLock.Wait(); + try { StopProcessCoreAsync(updateStatus: false, CancellationToken.None).GetAwaiter().GetResult(); } + finally { lifecycleLock.Release(); } + } + catch + { + } + recoveryCancellation.Dispose(); + lifecycleLock.Dispose(); + ownedComponentManager?.Dispose(); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(disposed, this); + } + + private sealed class ResolverSummaryDto + { + public string Name { get; set; } + public string Proto { get; set; } + public bool IPv6 { get; set; } + public bool? Dnssec { get; set; } + public bool NoLog { get; set; } + public bool NoFilter { get; set; } + public string Description { get; set; } + public string[] Addrs { get; set; } + public int[] Ports { get; set; } + public string Stamp { get; set; } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeModels.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeModels.cs new file mode 100644 index 000000000..7d1b0c76a --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimeModels.cs @@ -0,0 +1,117 @@ +#nullable enable +using System; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller.Service +{ + public enum DnsCryptRuntimeState + { + NotInstalled = 0, + Stopped = 1, + Starting = 2, + Running = 3, + Updating = 4, + Failed = 5, + } + + public sealed record DnsCryptRuntimeStatus( + DnsCryptRuntimeState State, + Version? Version, + int ProcessId, + int Port, + DateTimeOffset? StartedAtUtc, + string? ConfigPath, + string? LastError) + { + public bool IsRunning => State == DnsCryptRuntimeState.Running; + public bool IsServing => ProcessId > 0 && (State == DnsCryptRuntimeState.Running || State == DnsCryptRuntimeState.Updating); + } + + public sealed record DnsCryptRuntimeStartOptions( + DnsCryptConfig Config, + int? ShadowsocksSocks5Port = null) + { + public string ShadowsocksSocks5Host { get; init; } = "127.0.0.1"; + + public System.Collections.Generic.IReadOnlyDictionary StaticResolverStamps { get; init; } + = new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public sealed record DnsCryptResolverInfo( + string Name, + string Protocol, + bool IPv6, + bool? Dnssec, + bool NoLog, + bool NoFilter, + string Description, + System.Collections.Generic.IReadOnlyList Addresses, + string CountryCode = "", + string CountryName = "", + string FlagEmoji = "", + int? LatencyMs = null) + { + public System.Collections.Generic.IReadOnlyList Ports { get; init; } = System.Array.Empty(); + public string Stamp { get; init; } = string.Empty; + } + + public sealed class DnsCryptBootstrapException : InvalidOperationException + { + public DnsCryptBootstrapException() { } + public DnsCryptBootstrapException(string message) : base(message) { } + public DnsCryptBootstrapException(string message, Exception? innerException) : base(message, innerException) { } + } + + public sealed class DnsCryptComponentNetworkException : InvalidOperationException + { + public DnsCryptComponentNetworkException() { } + public DnsCryptComponentNetworkException(string message) : base(message) { } + public DnsCryptComponentNetworkException(string message, Exception? innerException) : base(message, innerException) { } + } + + public sealed class DnsCryptRuntimeStartupException : InvalidOperationException + { + public DnsCryptRuntimeStartupException() { } + public DnsCryptRuntimeStartupException(string message) : base(message) { } + public DnsCryptRuntimeStartupException(string message, Exception? innerException) : base(message, innerException) { } + } + + public sealed class DnsCryptRuntimeStatusChangedEventArgs : EventArgs + { + public DnsCryptRuntimeStatusChangedEventArgs(DnsCryptRuntimeStatus status) + { + ArgumentNullException.ThrowIfNull(status); + Status = status; + } + + public DnsCryptRuntimeStatus Status { get; } + } + + internal sealed record DnsCryptCommandResult(int ExitCode, string StandardOutput, string StandardError); + + internal interface IDnsCryptRunningProcess : IDisposable + { + int Id { get; } + bool HasExited { get; } + event EventHandler? Exited; + void Kill(); + System.Threading.Tasks.Task WaitForExitAsync(System.Threading.CancellationToken cancellationToken); + } + + internal interface IDnsCryptRuntimePlatform + { + System.Threading.Tasks.Task ExecuteAsync( + string executablePath, + string workingDirectory, + System.Collections.Generic.IReadOnlyList arguments, + TimeSpan timeout, + System.Threading.CancellationToken cancellationToken); + + IDnsCryptRunningProcess Start( + string executablePath, + string workingDirectory, + System.Collections.Generic.IReadOnlyList arguments, + Action standardOutput, + Action standardError); + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimePlatform.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimePlatform.cs new file mode 100644 index 000000000..bcf8dde24 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptRuntimePlatform.cs @@ -0,0 +1,161 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Util.ProcessManagement; + +namespace Shadowsocks.Controller.Service +{ + internal sealed class SystemDnsCryptRuntimePlatform : IDnsCryptRuntimePlatform + { + public async Task ExecuteAsync( + string executablePath, + string workingDirectory, + IReadOnlyList arguments, + TimeSpan timeout, + CancellationToken cancellationToken) + { + using Process process = CreateProcess(executablePath, workingDirectory, arguments); + if (!process.Start()) + throw new InvalidOperationException($"Unable to start '{executablePath}'."); + Task stdout = process.StandardOutput.ReadToEndAsync(cancellationToken); + Task stderr = process.StandardError.ReadToEndAsync(cancellationToken); + try + { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(timeout); + await process.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + TryKill(process); + throw new TimeoutException( + $"'{Path.GetFileName(executablePath)} {string.Join(" ", arguments)}' timed out " + + $"after {timeout.TotalSeconds:0} seconds."); + } + catch (OperationCanceledException) + { + TryKill(process); + throw; + } + string outputText = await stdout.ConfigureAwait(false); + string errorText = await stderr.ConfigureAwait(false); + return new DnsCryptCommandResult(process.ExitCode, outputText, errorText); + } + + public IDnsCryptRunningProcess Start( + string executablePath, + string workingDirectory, + IReadOnlyList arguments, + Action standardOutput, + Action standardError) + { + Process process = CreateProcess(executablePath, workingDirectory, arguments); + process.EnableRaisingEvents = true; + if (standardOutput is not null) + process.OutputDataReceived += (_, args) => { if (args.Data is not null) standardOutput(args.Data); }; + if (standardError is not null) + process.ErrorDataReceived += (_, args) => { if (args.Data is not null) standardError(args.Data); }; + if (!process.Start()) + { + process.Dispose(); + throw new InvalidOperationException($"Unable to start '{executablePath}'."); + } + try + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + return new SystemDnsCryptRunningProcess(process); + } + catch + { + TryKill(process); + process.Dispose(); + throw; + } + } + + private static Process CreateProcess(string executablePath, string workingDirectory, IReadOnlyList arguments) + { + var startInfo = new ProcessStartInfo + { + FileName = executablePath, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + foreach (string argument in arguments) + startInfo.ArgumentList.Add(argument); + return new Process { StartInfo = startInfo }; + } + + private static void TryKill(Process process) + { + try + { + if (!process.HasExited) + process.Kill(entireProcessTree: true); + } + catch + { + } + } + } + + internal sealed class SystemDnsCryptRunningProcess : IDnsCryptRunningProcess + { + private readonly Process process; + private readonly Job killOnCloseJob; + + public SystemDnsCryptRunningProcess(Process process) + { + ArgumentNullException.ThrowIfNull(process); + this.process = process; + killOnCloseJob = new Job(); + try + { + killOnCloseJob.AddProcessOrThrow(process, "DNSCrypt Proxy"); + } + catch + { + killOnCloseJob.Dispose(); + throw; + } + process.Exited += ForwardExited; + } + + public int Id => process.Id; + public bool HasExited + { + get + { + try { return process.HasExited; } + catch (InvalidOperationException) { return true; } + } + } + + public event EventHandler? Exited; + + public void Kill() + { + if (!HasExited) + process.Kill(entireProcessTree: true); + } + + public Task WaitForExitAsync(CancellationToken cancellationToken) => process.WaitForExitAsync(cancellationToken); + + public void Dispose() + { + process.Exited -= ForwardExited; + killOnCloseJob.Dispose(); + process.Dispose(); + } + + private void ForwardExited(object? sender, EventArgs e) => Exited?.Invoke(this, EventArgs.Empty); + } +} diff --git a/Shadowsocks.Windows/Controller/Service/DnsCryptTomlGenerator.cs b/Shadowsocks.Windows/Controller/Service/DnsCryptTomlGenerator.cs new file mode 100644 index 000000000..f9b78ba05 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/DnsCryptTomlGenerator.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller.Service +{ + internal enum DnsCryptTomlPurpose + { + Runtime = 0, + ResolverCatalog = 1, + } + + internal static class DnsCryptTomlGenerator + { + internal const string PublicResolversMinisignKey = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"; + private static readonly Regex ServerNameRegex = new( + @"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + internal static string Generate( + int listenPort, + DnsCryptConfig config, + int? shadowsocksSocks5Port = null, + DnsCryptTomlPurpose purpose = DnsCryptTomlPurpose.Runtime, + IReadOnlyDictionary staticResolverStamps = null) + { + if (listenPort is < 1 or > 65535) + throw new ArgumentOutOfRangeException(nameof(listenPort)); + ArgumentNullException.ThrowIfNull(config); + if (!config.ipv4Servers && !config.ipv6Servers) + throw new ArgumentException("At least one DNSCrypt address family must be enabled.", nameof(config)); + + bool catalogMode = purpose == DnsCryptTomlPurpose.ResolverCatalog; + // Resolver-catalog mode only reads the pre-seeded signed cache and performs no + // upstream DNS traffic, so the user's active-runtime proxy preference is irrelevant. + bool routeThroughShadowsocks = !catalogMode && config.routeThroughShadowsocks; + if (routeThroughShadowsocks && (!shadowsocksSocks5Port.HasValue || shadowsocksSocks5Port.Value is < 1 or > 65535)) + throw new ArgumentException( + "A valid local Shadowsocks SOCKS5 port is required when DNSCrypt proxy routing is enabled.", + nameof(shadowsocksSocks5Port)); + string[] serverNames = NormalizeServerNames(config.serverNames); + if (!catalogMode && serverNames.Length == 0) + { + throw new ArgumentException( + "A DNSCrypt runtime requires at least one resolver selected from the signed catalog.", + nameof(config)); + } + Dictionary pinnedStamps = catalogMode + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : NormalizeStaticResolverStamps(serverNames, staticResolverStamps); + var builder = new StringBuilder(3000); + builder.AppendLine("# Generated by Shadowsocks Reborn. Do not edit while Shadowsocks is running."); + if (serverNames.Length > 0) + builder.Append("server_names = [").Append(string.Join(", ", serverNames.Select(ToTomlLiteral))).AppendLine("]"); + builder.Append("listen_addresses = ['127.0.0.1:").Append(listenPort).AppendLine("']"); + builder.AppendLine("max_clients = 250"); + builder.Append("ipv4_servers = ").AppendLine(ToTomlBoolean(config.ipv4Servers)); + builder.Append("ipv6_servers = ").AppendLine(ToTomlBoolean(config.ipv6Servers)); + // Runtime server_names constrains dnscrypt-proxy to the concrete automatic/manual + // resolver set selected from the signed catalog before startup. + builder.AppendLine("dnscrypt_servers = true"); + builder.AppendLine("doh_servers = true"); + builder.AppendLine("odoh_servers = false"); + builder.Append("require_dnssec = ").AppendLine(ToTomlBoolean(config.requireDnssec)); + builder.Append("require_nolog = ").AppendLine(ToTomlBoolean(config.requireNoLog)); + builder.Append("require_nofilter = ").AppendLine(ToTomlBoolean(config.requireNoFilter)); + builder.Append("force_tcp = ").AppendLine(ToTomlBoolean(routeThroughShadowsocks)); + if (routeThroughShadowsocks) + { + builder.Append("proxy = 'socks5://127.0.0.1:").Append(shadowsocksSocks5Port.Value).AppendLine("'"); + } + builder.AppendLine("http3 = false"); + builder.AppendLine("timeout = 5000"); + builder.AppendLine("keepalive = 30"); + builder.AppendLine("log_level = 2"); + builder.AppendLine("use_syslog = false"); + builder.AppendLine("pqdnscrypt = true"); + // Both catalog parsing and the active runtime are network-bootstrap-free inside + // dnscrypt-proxy. Shadowsocks Reborn refreshes the signed resolver catalog itself + // through Cloudflare DoH over Shadowsocks, with Google DoH as fallback. + builder.AppendLine("bootstrap_resolvers = []"); + builder.AppendLine("ignore_system_dns = true"); + builder.AppendLine("netprobe_timeout = 0"); + builder.AppendLine("netprobe_address = '1.1.1.1:443'"); + builder.AppendLine("block_ipv6 = false"); + builder.AppendLine("block_unqualified = true"); + builder.AppendLine("block_undelegated = true"); + builder.AppendLine("cache = true"); + builder.AppendLine("cache_size = 4096"); + builder.AppendLine("enable_hot_reload = false"); + builder.AppendLine(); + if (catalogMode) + AppendPublicResolverSource(builder); + else + AppendStaticResolvers(builder, serverNames, pinnedStamps); + return builder.ToString(); + } + + internal static bool RuntimeUsesPlaintextBootstrap(string toml) + { + if (string.IsNullOrWhiteSpace(toml)) + return true; + return !toml.Contains("bootstrap_resolvers = []", StringComparison.Ordinal) + || !toml.Contains("ignore_system_dns = true", StringComparison.Ordinal); + } + + + private static Dictionary NormalizeStaticResolverStamps( + string[] serverNames, + IReadOnlyDictionary staticResolverStamps) + { + if (staticResolverStamps is null) + { + throw new ArgumentException( + "An active DNSCrypt runtime requires signed resolver stamps and must not refresh remote resolver sources.", + nameof(staticResolverStamps)); + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string serverName in serverNames) + { + if (!staticResolverStamps.TryGetValue(serverName, out string stamp)) + { + throw new ArgumentException( + $"The signed DNSCrypt resolver stamp for '{serverName}' is unavailable.", + nameof(staticResolverStamps)); + } + + string value = stamp?.Trim(); + if (string.IsNullOrWhiteSpace(value) + || !value.StartsWith("sdns://", StringComparison.Ordinal) + || value.IndexOfAny(['\r', '\n', '\'']) >= 0) + { + throw new ArgumentException( + $"The signed DNSCrypt resolver stamp for '{serverName}' is invalid.", + nameof(staticResolverStamps)); + } + result[serverName] = value; + } + return result; + } + + private static void AppendStaticResolvers( + StringBuilder builder, + string[] serverNames, + Dictionary staticResolverStamps) + { + builder.AppendLine("[static]"); + foreach (string serverName in serverNames) + { + builder.Append("[static.'").Append(serverName).AppendLine("']"); + builder.Append("stamp = ").AppendLine(ToTomlLiteral(staticResolverStamps[serverName])); + } + } + + private static void AppendPublicResolverSource(StringBuilder builder) + { + builder.AppendLine("[sources]"); + builder.AppendLine("[sources.public-resolvers]"); + // The application downloads and verifies this source before starting + // dnscrypt-proxy. Empty URLs are intentional: dnscrypt-proxy may only + // read the local authenticated cache pair and cannot fall back to DNS. + builder.AppendLine("urls = []"); + builder.AppendLine("cache_file = 'public-resolvers.md'"); + builder.Append("minisign_key = '").Append(PublicResolversMinisignKey).AppendLine("'"); + builder.AppendLine("refresh_delay = 168"); + builder.AppendLine("cache_ttl = 168"); + builder.AppendLine("prefix = ''"); + } + + internal static string[] NormalizeServerNames(IEnumerable serverNames) + { + if (serverNames is null) + return []; + + var result = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string candidate in serverNames) + { + string value = candidate?.Trim(); + if (string.IsNullOrWhiteSpace(value)) + continue; + if (!ServerNameRegex.IsMatch(value)) + throw new ArgumentException($"DNSCrypt resolver name '{value}' contains unsupported characters.", nameof(serverNames)); + if (seen.Add(value)) + result.Add(value); + } + return result.ToArray(); + } + + private static string ToTomlBoolean(bool value) => value ? "true" : "false"; + private static string ToTomlLiteral(string value) => "'" + value + "'"; + } +} diff --git a/Shadowsocks.Windows/Controller/Service/IpCountryService.cs b/Shadowsocks.Windows/Controller/Service/IpCountryService.cs new file mode 100644 index 000000000..090f27b28 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/IpCountryService.cs @@ -0,0 +1,380 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller.Service +{ + public sealed record IpCountryInfo(string CountryCode, string CountryName, string FlagEmoji, string Address); + + internal static class IpCountryService + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly ConcurrentDictionary Cache = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary NegativeCache = new(StringComparer.OrdinalIgnoreCase); + private static readonly SemaphoreSlim LookupGate = new(4, 4); + private static readonly HttpClient DirectClient = CreateClient(useProxy: false); + private static readonly HttpClient ProxyFallbackClient = CreateClient(useProxy: true); + private static readonly TimeSpan NegativeCacheLifetime = TimeSpan.FromMinutes(10); + private static readonly HttpClient[] LookupClients = [DirectClient, ProxyFallbackClient]; + + public static async Task ResolveHostAsync(string host, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(host)) + return null; + + string normalized = NormalizeHost(host); + if (string.IsNullOrWhiteSpace(normalized)) + return null; + if (IPAddress.TryParse(normalized, out IPAddress? parsed)) + return await LookupAddressAsync(parsed, cancellationToken).ConfigureAwait(false); + + try + { + IPAddress[] addresses = await Dns.GetHostAddressesAsync(normalized, cancellationToken).ConfigureAwait(false); + IPAddress? address = addresses.FirstOrDefault(item => item.AddressFamily == AddressFamily.InterNetwork) + ?? addresses.FirstOrDefault(); + return address is null ? null : await LookupAddressAsync(address, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + Logger.Debug(exception, $"IP country lookup could not resolve host '{normalized}'."); + return null; + } + } + + public static async Task LookupAddressAsync(IPAddress address, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(address); + if (!IsPublicAddress(address)) + return null; + + string key = address.ToString(); + if (Cache.TryGetValue(key, out IpCountryInfo? cached)) + return cached; + if (NegativeCache.TryGetValue(key, out DateTimeOffset failedAt) + && DateTimeOffset.UtcNow - failedAt < NegativeCacheLifetime) + return null; + + await LookupGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (Cache.TryGetValue(key, out cached)) + return cached; + + IpCountryInfo? info = await LookupCountryIsAsync(key, cancellationToken).ConfigureAwait(false) + ?? await LookupIpWhoAsync(key, cancellationToken).ConfigureAwait(false); + if (info is not null) + { + Cache[key] = info; + NegativeCache.TryRemove(key, out _); + return info; + } + + NegativeCache[key] = DateTimeOffset.UtcNow; + Logger.Debug($"IP country lookup returned no result for {key}."); + return null; + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + NegativeCache[key] = DateTimeOffset.UtcNow; + Logger.Debug(exception, $"IP country lookup failed for {key}."); + return null; + } + finally + { + LookupGate.Release(); + } + } + + public static async Task> LookupAddressesAsync( + IEnumerable addresses, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(addresses); + string[] keys = addresses + .Where(address => address is not null && IsPublicAddress(address)) + .Select(address => address.ToString()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var missing = new List(); + foreach (string key in keys) + { + if (Cache.TryGetValue(key, out IpCountryInfo? cached)) + { + result[key] = cached; + continue; + } + if (NegativeCache.TryGetValue(key, out DateTimeOffset failedAt) + && DateTimeOffset.UtcNow - failedAt < NegativeCacheLifetime) + continue; + missing.Add(key); + } + + string[][] chunks = missing.Chunk(100).ToArray(); + Task>[] batchTasks = chunks + .Select(chunk => LookupCountryIsBatchAsync(chunk, cancellationToken)) + .ToArray(); + IReadOnlyDictionary[] batches = await Task.WhenAll(batchTasks).ConfigureAwait(false); + + for (int index = 0; index < chunks.Length; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + string[] chunk = chunks[index]; + IReadOnlyDictionary batch = batches[index]; + foreach ((string key, IpCountryInfo info) in batch) + { + Cache[key] = info; + NegativeCache.TryRemove(key, out _); + result[key] = info; + } + + // If the primary batch endpoint failed completely, do not fan out into + // up to 100 secondary HTTP requests. Leave the entries uncached so a later + // catalog refresh or the targeted Automatic-selection fallback can retry. + if (batch.Count == 0) + continue; + + // For a partial batch response, fall back only for the missing IPs. + // Country classification must never be inferred from resolver text. + foreach (string key in chunk.Where(key => !batch.ContainsKey(key))) + { + cancellationToken.ThrowIfCancellationRequested(); + IpCountryInfo? info; + try + { + info = await LookupIpWhoAsync(key, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + Logger.Debug(exception, $"Secondary IP country lookup failed for {key}."); + info = null; + } + + if (info is not null) + { + Cache[key] = info; + NegativeCache.TryRemove(key, out _); + result[key] = info; + } + else + { + NegativeCache[key] = DateTimeOffset.UtcNow; + } + } + } + + return result; + } + + private static async Task LookupCountryIsAsync(string address, CancellationToken cancellationToken) + { + string? json = await GetStringWithFallbackAsync( + $"https://api.country.is/{Uri.EscapeDataString(address)}", + cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(json)) + return null; + CountryIsResponse? dto = JsonSerializer.Deserialize(json); + return CreateInfo(dto?.Country, address); + } + + private static async Task> LookupCountryIsBatchAsync( + string[] addresses, + CancellationToken cancellationToken) + { + if (addresses.Length == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + try + { + string payload = JsonSerializer.Serialize(addresses); + string? json = await PostStringWithFallbackAsync( + "https://api.country.is/", payload, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(json)) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + List? rows = JsonSerializer.Deserialize>(json); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (CountryIsResponse row in rows ?? []) + { + if (string.IsNullOrWhiteSpace(row.Ip)) + continue; + IpCountryInfo? info = CreateInfo(row.Country, row.Ip); + if (info is not null) + result[row.Ip] = info; + } + return result; + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + Logger.Debug(exception, "Batch IP country lookup failed."); + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } + + private static async Task LookupIpWhoAsync(string address, CancellationToken cancellationToken) + { + string? json = await GetStringWithFallbackAsync( + $"https://ipwho.is/{Uri.EscapeDataString(address)}?fields=success,country,country_code", + cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(json)) + return null; + IpWhoResponse? dto = JsonSerializer.Deserialize(json); + if (dto?.Success != true || string.IsNullOrWhiteSpace(dto.CountryCode)) + return null; + + string code = dto.CountryCode.Trim().ToUpperInvariant(); + return new IpCountryInfo(code, dto.Country?.Trim() ?? CountryCodeToName(code), CountryCodeToFlag(code), address); + } + + private static IpCountryInfo? CreateInfo(string? countryCode, string address) + { + if (string.IsNullOrWhiteSpace(countryCode)) + return null; + string code = countryCode.Trim().ToUpperInvariant(); + string flag = CountryCodeToFlag(code); + if (flag.Length == 0) + return null; + return new IpCountryInfo(code, CountryCodeToName(code), flag, address); + } + + internal static string NormalizeHost(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return string.Empty; + + string value = host.Trim(); + if (value.StartsWith('[')) + { + int closingBracket = value.IndexOf(']'); + if (closingBracket > 1) + return value[1..closingBracket]; + } + + if (IPAddress.TryParse(value, out _)) + return value; + + if (Uri.TryCreate($"tcp://{value}", UriKind.Absolute, out Uri? endpointUri) + && !string.IsNullOrWhiteSpace(endpointUri.Host)) + return endpointUri.Host; + + return value; + } + + internal static string CountryCodeToFlag(string? countryCode) + { + if (string.IsNullOrWhiteSpace(countryCode)) + return string.Empty; + string code = countryCode.Trim().ToUpperInvariant(); + if (code.Length != 2 || code.Any(ch => ch < 'A' || ch > 'Z')) + return string.Empty; + return string.Concat(code.Select(ch => char.ConvertFromUtf32(0x1F1E6 + ch - 'A'))); + } + + internal static string CountryCodeToName(string countryCode) + { + try + { + return new RegionInfo(countryCode).EnglishName; + } + catch (ArgumentException) + { + return countryCode; + } + } + + internal static bool IsPublicAddress(IPAddress address) + { + if (IPAddress.IsLoopback(address) || address.IsIPv6LinkLocal || address.IsIPv6Multicast || address.IsIPv6SiteLocal) + return false; + if (address.AddressFamily != AddressFamily.InterNetwork) + return true; + + byte[] bytes = address.GetAddressBytes(); + if (bytes[0] == 10 || bytes[0] == 127 || (bytes[0] == 169 && bytes[1] == 254) + || (bytes[0] == 192 && bytes[1] == 168) + || (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31)) + return false; + return true; + } + + private static async Task GetStringWithFallbackAsync(string uri, CancellationToken cancellationToken) + { + foreach (HttpClient client in LookupClients) + { + try + { + using HttpResponseMessage response = await client.GetAsync(uri, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + Logger.Debug(exception, $"Country metadata request failed: {uri}"); + } + } + return null; + } + + private static async Task PostStringWithFallbackAsync( + string uri, + string payload, + CancellationToken cancellationToken) + { + foreach (HttpClient client in LookupClients) + { + try + { + using var content = new StringContent(payload, Encoding.UTF8, "application/json"); + using HttpResponseMessage response = await client.PostAsync(uri, content, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + Logger.Debug(exception, $"Country metadata batch request failed: {uri}"); + } + } + return null; + } + + private static HttpClient CreateClient(bool useProxy) + { + var handler = new SocketsHttpHandler { UseProxy = useProxy }; + var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(6) }; + client.DefaultRequestHeaders.UserAgent.ParseAdd($"Shadowsocks-Reborn/{ApplicationInfo.Version}"); + return client; + } + + private sealed class CountryIsResponse + { + [JsonPropertyName("ip")] + public string? Ip { get; set; } + [JsonPropertyName("country")] + public string? Country { get; set; } + } + + private sealed class IpWhoResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + [JsonPropertyName("country")] + public string? Country { get; set; } + [JsonPropertyName("country_code")] + public string? CountryCode { get; set; } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/ManagedHttpProxyService.cs b/Shadowsocks.Windows/Controller/Service/ManagedHttpProxyService.cs new file mode 100644 index 000000000..53986b600 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/ManagedHttpProxyService.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Controller.Traffic.Applications; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Managed HTTP/1.1 proxy replacing the bundled Privoxy executable. + /// Supports CONNECT tunnelling and ordinary absolute-form HTTP requests. + /// Routing decisions are application-aware and shared with Admin Mode. + /// + internal sealed class ManagedHttpProxyService : Listener.Service + { + private const int MaxHeaderBytes = 64 * 1024; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly TrafficPolicyEngine _policyEngine; + private readonly ConcurrentDictionary _connections = new(); + private Configuration _configuration; + private long _nextConnectionId; + private volatile bool _stopping; + + public ManagedHttpProxyService(TrafficPolicyEngine policyEngine, Configuration initialConfiguration) + { + ArgumentNullException.ThrowIfNull(policyEngine); + ArgumentNullException.ThrowIfNull(initialConfiguration); + _policyEngine = policyEngine; + _configuration = initialConfiguration; + } + + public void UpdateConfiguration(Configuration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + _configuration = configuration; + } + + public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) + { + if (socket.ProtocolType != ProtocolType.Tcp || !LooksLikeHttp(firstPacket, length)) + { + return false; + } + + if (_stopping) + { + socket.Dispose(); + return true; + } + + long id = Interlocked.Increment(ref _nextConnectionId); + byte[] initial = firstPacket.AsSpan(0, length).ToArray(); + ProcessIdentity identity = TcpProcessResolver.Resolve(socket); + Connection connection = new( + id, + socket, + initial, + identity, + _policyEngine, + _configuration, + RemoveConnection); + _connections[id] = connection; + connection.Start(); + return true; + } + + public override void Shutdown() + { + _stopping = true; + foreach (Connection connection in _connections.Values) + { + connection.Dispose(); + } + + _connections.Clear(); + } + + private void RemoveConnection(long id) + { + _connections.TryRemove(id, out _); + } + + private static bool LooksLikeHttp(byte[] packet, int length) + { + if (length < 4) + { + return false; + } + + ReadOnlySpan span = packet.AsSpan(0, Math.Min(length, 16)); + return StartsWithAscii(span, "CONNECT ") + || StartsWithAscii(span, "GET ") + || StartsWithAscii(span, "POST ") + || StartsWithAscii(span, "PUT ") + || StartsWithAscii(span, "DELETE ") + || StartsWithAscii(span, "HEAD ") + || StartsWithAscii(span, "OPTIONS ") + || StartsWithAscii(span, "PATCH ") + || StartsWithAscii(span, "TRACE "); + } + + internal static string BuildRoutingUrlForPolicy( + string target, + string host, + int port, + bool isConnect) + { + if (!isConnect && Uri.TryCreate(target, UriKind.Absolute, out Uri absoluteUri)) + { + return absoluteUri.AbsoluteUri; + } + + string normalizedHost = host.Contains(':') ? $"[{host}]" : host; + string scheme = isConnect ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + int defaultPort = isConnect ? 443 : 80; + string portText = port == defaultPort ? string.Empty : $":{port}"; + string path = isConnect || string.IsNullOrWhiteSpace(target) || !target.StartsWith('/') + ? "/" + : target; + return $"{scheme}://{normalizedHost}{portText}{path}"; + } + + private static bool StartsWithAscii(ReadOnlySpan value, string prefix) + { + if (value.Length < prefix.Length) + { + return false; + } + + for (int i = 0; i < prefix.Length; i++) + { + if (value[i] != (byte)prefix[i]) + { + return false; + } + } + + return true; + } + + private sealed class Connection( + long id, + Socket client, + byte[] initialPacket, + ProcessIdentity process, + TrafficPolicyEngine policyEngine, + Configuration configuration, + Action onClosed) : IDisposable + { + private readonly long _id = id; + private readonly Socket _client = client; + private readonly byte[] _initialPacket = initialPacket; + private readonly ProcessIdentity _process = process; + private readonly TrafficPolicyEngine _policyEngine = policyEngine; + private readonly Configuration _configuration = configuration; + private readonly Action _onClosed = onClosed; + private readonly CancellationTokenSource _shutdown = new(); + private Socket _upstream; + private int _disposed; + + public void Start() + { + _ = RunAsync(); + } + + private async Task RunAsync() + { + try + { + using NetworkStream clientStream = new(_client, ownsSocket: false); + byte[] requestBytes = await ReadHeadersAsync(clientStream, _initialPacket, _shutdown.Token).ConfigureAwait(false); + HttpProxyRequest request = HttpProxyRequest.Parse(requestBytes); + + TrafficContext context = new() + { + ProcessId = _process.ProcessId, + ProcessPath = _process.ProcessPath, + ProcessName = _process.ProcessName, + Protocol = "TCP", + DestinationHost = request.Host, + DestinationUrl = request.RoutingUrl, + DestinationPort = request.Port, + IsInternal = _process.IsCurrentProcess, + }; + RouteDecision decision = _policyEngine.EvaluateUserMode(context); + Logger.Debug( + "Managed HTTP route {0} for {1}:{2} (pid={3}, process={4}): {5}", + decision.Action, + request.Host, + request.Port, + _process.ProcessId, + _process.ProcessName ?? string.Empty, + decision.Reason); + + if (decision.Action == TrafficRouteAction.Block) + { + await WriteErrorAsync(clientStream, 403, "Blocked by application routing policy", _shutdown.Token).ConfigureAwait(false); + return; + } + + _upstream = decision.Action == TrafficRouteAction.Direct + ? await ConnectDirectAsync(request.Host, request.Port, _shutdown.Token).ConfigureAwait(false) + : await Socks5Connector.ConnectAsync( + _configuration.LocalHost, + _configuration.localPort, + request.Host, + request.Port, + _shutdown.Token).ConfigureAwait(false); + + using NetworkStream upstreamStream = new(_upstream, ownsSocket: false); + if (request.IsConnect) + { + byte[] established = "HTTP/1.1 200 Connection Established\r\nProxy-Agent: Shadowsocks.NET\r\n\r\n"u8.ToArray(); + await clientStream.WriteAsync(established, _shutdown.Token).ConfigureAwait(false); + + // A client may optimistically send tunnel bytes in the same TCP + // packet as the CONNECT headers. Preserve them instead of dropping + // the bytes already consumed by ReadHeadersAsync. + if (requestBytes.Length > request.HeaderEnd) + { + await upstreamStream.WriteAsync( + requestBytes.AsMemory(request.HeaderEnd), + _shutdown.Token).ConfigureAwait(false); + } + } + else + { + byte[] rewritten = request.RewriteForOriginServer(requestBytes); + await upstreamStream.WriteAsync(rewritten, _shutdown.Token).ConfigureAwait(false); + } + + Task clientToUpstream = PumpAsync(clientStream, upstreamStream, _shutdown.Token); + Task upstreamToClient = PumpAsync(upstreamStream, clientStream, _shutdown.Token); + await Task.WhenAny(clientToUpstream, upstreamToClient).ConfigureAwait(false); + _shutdown.Cancel(); + await IgnoreShutdownAsync(clientToUpstream).ConfigureAwait(false); + await IgnoreShutdownAsync(upstreamToClient).ConfigureAwait(false); + } + catch (OperationCanceledException) when (_shutdown.IsCancellationRequested) + { + } + catch (Exception exception) + { + Logger.Debug(exception, "Managed HTTP proxy connection failed"); + try + { + using NetworkStream clientStream = new(_client, ownsSocket: false); + await WriteErrorAsync(clientStream, 502, "Proxy connection failed", CancellationToken.None).ConfigureAwait(false); + } + catch + { + } + } + finally + { + Dispose(); + } + } + + private static async Task ConnectDirectAsync(string host, int port, CancellationToken cancellationToken) + { + Socket socket = new(SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + try + { + await socket.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); + return socket; + } + catch + { + socket.Dispose(); + throw; + } + } + + private static async Task ReadHeadersAsync(NetworkStream stream, byte[] initial, CancellationToken cancellationToken) + { + using MemoryStream buffer = new(Math.Min(MaxHeaderBytes, Math.Max(4096, initial.Length + 1024))); + buffer.Write(initial, 0, initial.Length); + if (FindHeaderEnd(initial) >= 0) + { + return buffer.ToArray(); + } + + byte[] chunk = new byte[4096]; + while (buffer.Length < MaxHeaderBytes) + { + int read = await stream.ReadAsync(chunk, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException("Client disconnected before completing HTTP proxy headers."); + } + + buffer.Write(chunk, 0, read); + byte[] current = buffer.GetBuffer(); + if (FindHeaderEnd(current.AsSpan(0, checked((int)buffer.Length))) >= 0) + { + return buffer.ToArray(); + } + } + + throw new InvalidDataException($"HTTP proxy headers exceed {MaxHeaderBytes} bytes."); + } + + private static int FindHeaderEnd(ReadOnlySpan bytes) + { + return bytes.IndexOf("\r\n\r\n"u8); + } + + private static async Task PumpAsync(NetworkStream source, NetworkStream destination, CancellationToken cancellationToken) + { + byte[] buffer = new byte[32 * 1024]; + while (true) + { + int read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + return; + } + + await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + } + } + + private static async Task IgnoreShutdownAsync(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (IOException) + { + } + catch (SocketException) + { + } + } + + private static async Task WriteErrorAsync(NetworkStream stream, int status, string message, CancellationToken cancellationToken) + { + string body = message + "\r\n"; + string response = $"HTTP/1.1 {status} {message}\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {Encoding.UTF8.GetByteCount(body)}\r\n\r\n{body}"; + await stream.WriteAsync(Encoding.UTF8.GetBytes(response), cancellationToken).ConfigureAwait(false); + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + _shutdown.Cancel(); + _upstream?.Dispose(); + _client.Dispose(); + _shutdown.Dispose(); + _onClosed(_id); + } + } + + private sealed class HttpProxyRequest( + string initialMethod, + string initialTarget, + string initialHost, + int initialPort, + bool initialIsConnect, + int initialHeaderEnd) + { + public string Method { get; } = initialMethod; + public string Target { get; } = initialTarget; + public string Host { get; } = initialHost; + public int Port { get; } = initialPort; + public bool IsConnect { get; } = initialIsConnect; + public int HeaderEnd { get; } = initialHeaderEnd; + public string RoutingUrl => BuildRoutingUrl(); + + public static HttpProxyRequest Parse(byte[] requestBytes) + { + int headerEnd = requestBytes.AsSpan().IndexOf("\r\n\r\n"u8); + if (headerEnd < 0) + { + throw new InvalidDataException("Incomplete HTTP proxy request."); + } + + string headers = Encoding.Latin1.GetString(requestBytes, 0, headerEnd + 4); + int firstLineEnd = headers.IndexOf("\r\n", StringComparison.Ordinal); + if (firstLineEnd <= 0) + { + throw new InvalidDataException("Invalid HTTP request line."); + } + + string firstLine = headers[..firstLineEnd]; + string[] parts = firstLine.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 3) + { + throw new InvalidDataException("Invalid HTTP proxy request line."); + } + + string method = parts[0]; + string target = parts[1]; + bool connect = method.Equals("CONNECT", StringComparison.OrdinalIgnoreCase); + if (connect) + { + (string host, int port) = ParseAuthority(target, 443); + return new HttpProxyRequest(method, target, host, port, true, headerEnd + 4); + } + + if (Uri.TryCreate(target, UriKind.Absolute, out Uri absoluteUri)) + { + if (!absoluteUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException( + $"Unsupported HTTP proxy URI scheme '{absoluteUri.Scheme}'. HTTPS must use CONNECT."); + } + + int port = absoluteUri.IsDefaultPort ? 80 : absoluteUri.Port; + return new HttpProxyRequest(method, target, absoluteUri.Host, port, false, headerEnd + 4); + } + + string hostHeader = FindHeader(headers, "Host") + ?? throw new InvalidDataException("HTTP proxy request does not contain a Host header."); + (string fallbackHost, int fallbackPort) = ParseAuthority(hostHeader, 80); + return new HttpProxyRequest(method, target, fallbackHost, fallbackPort, false, headerEnd + 4); + } + + private string BuildRoutingUrl() + => ManagedHttpProxyService.BuildRoutingUrlForPolicy(Target, Host, Port, IsConnect); + + public byte[] RewriteForOriginServer(byte[] requestBytes) + { + if (IsConnect) + { + return requestBytes; + } + + string headers = Encoding.Latin1.GetString(requestBytes, 0, HeaderEnd); + int firstLineEnd = headers.IndexOf("\r\n", StringComparison.Ordinal); + if (firstLineEnd <= 0) + { + throw new InvalidDataException("Invalid HTTP request line."); + } + + string version = headers[..firstLineEnd].Split(' ', 3)[2]; + string originTarget = Target; + if (Uri.TryCreate(Target, UriKind.Absolute, out Uri uri)) + { + originTarget = string.IsNullOrEmpty(uri.PathAndQuery) ? "/" : uri.PathAndQuery; + } + + string[] lines = headers[(firstLineEnd + 2)..].Split("\r\n", StringSplitOptions.None); + bool isUpgrade = false; + StringBuilder rewritten = new(); + rewritten.Append(Method).Append(' ').Append(originTarget).Append(' ').Append(version).Append("\r\n"); + + foreach (string line in lines) + { + if (line.Length == 0) + { + continue; + } + + int separator = line.IndexOf(':'); + if (separator <= 0) + { + continue; + } + + string name = line[..separator].Trim(); + if (name.Equals("Proxy-Connection", StringComparison.OrdinalIgnoreCase) + || name.Equals("Proxy-Authorization", StringComparison.OrdinalIgnoreCase) + || name.Equals("Keep-Alive", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (name.Equals("Upgrade", StringComparison.OrdinalIgnoreCase)) + { + isUpgrade = true; + } + + if (name.Equals("Connection", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + rewritten.Append(line).Append("\r\n"); + } + + rewritten.Append(isUpgrade ? "Connection: Upgrade\r\n" : "Connection: close\r\n"); + rewritten.Append("\r\n"); + byte[] headerBytes = Encoding.Latin1.GetBytes(rewritten.ToString()); + + if (requestBytes.Length == HeaderEnd) + { + return headerBytes; + } + + byte[] result = new byte[headerBytes.Length + requestBytes.Length - HeaderEnd]; + Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); + Buffer.BlockCopy(requestBytes, HeaderEnd, result, headerBytes.Length, requestBytes.Length - HeaderEnd); + return result; + } + + private static string FindHeader(string headers, string name) + { + string prefix = name + ":"; + string[] lines = headers.Split("\r\n", StringSplitOptions.None); + foreach (string line in lines) + { + if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return line[prefix.Length..].Trim(); + } + } + + return null; + } + + private static (string Host, int Port) ParseAuthority(string authority, int defaultPort) + { + string value = authority.Trim(); + if (value.StartsWith('[')) + { + int closing = value.IndexOf(']'); + if (closing <= 0) + { + throw new InvalidDataException("Invalid IPv6 authority."); + } + + string host = value[1..closing]; + if (closing + 1 < value.Length && value[closing + 1] == ':') + { + return (host, ParsePort(value[(closing + 2)..])); + } + + return (host, defaultPort); + } + + int colon = value.LastIndexOf(':'); + if (colon > 0 && value.IndexOf(':') == colon) + { + return (value[..colon], ParsePort(value[(colon + 1)..])); + } + + return (value, defaultPort); + } + + private static int ParsePort(string text) + { + if (!int.TryParse(text, out int port) || port is < 1 or > 65535) + { + throw new InvalidDataException("Invalid destination port in HTTP proxy request."); + } + + return port; + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/MinisignVerifier.cs b/Shadowsocks.Windows/Controller/Service/MinisignVerifier.cs new file mode 100644 index 000000000..310a08830 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/MinisignVerifier.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; + +namespace Shadowsocks.Controller.Service +{ + internal static class MinisignVerifier + { + private const string UntrustedCommentPrefix = "untrusted comment: "; + private const string TrustedCommentPrefix = "trusted comment: "; + private const int PublicKeyLength = 42; + private const int SignatureRecordLength = 74; + private const int Ed25519SignatureLength = 64; + private const int Blake2bHashLength = 64; + + public static bool VerifyFile(string filePath, string signatureText, string publicKeyBase64) + => VerifyFileCore(filePath, signatureText, publicKeyBase64, allowLegacy: false); + + internal static bool VerifyFileAllowLegacy(string filePath, string signatureText, string publicKeyBase64) + => VerifyFileCore(filePath, signatureText, publicKeyBase64, allowLegacy: true); + + private static bool VerifyFileCore( + string filePath, + string signatureText, + string publicKeyBase64, + bool allowLegacy) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("A signed file path is required.", nameof(filePath)); + if (!File.Exists(filePath)) + throw new FileNotFoundException("The signed file was not found.", filePath); + + ParsedPublicKey publicKey = ParsePublicKey(publicKeyBase64); + ParsedSignature signature = ParseSignature(signatureText); + if (signature.Mode == MinisignSignatureMode.Legacy && !allowLegacy) + throw new InvalidDataException("Legacy Minisign signatures are not allowed for this artifact."); + if (!publicKey.KeyId.SequenceEqual(signature.KeyId)) + return false; + + bool payloadSignatureValid = signature.Mode switch + { + MinisignSignatureMode.Prehashed => + VerifyEd25519(publicKey.PublicKey, ComputeBlake2b512(filePath), signature.Signature), + MinisignSignatureMode.Legacy => + VerifyEd25519File(publicKey.PublicKey, filePath, signature.Signature), + _ => false, + }; + if (!payloadSignatureValid) + return false; + + byte[] trustedComment = Encoding.UTF8.GetBytes(signature.TrustedComment); + byte[] globalMessage = new byte[signature.Signature.Length + trustedComment.Length]; + Buffer.BlockCopy(signature.Signature, 0, globalMessage, 0, signature.Signature.Length); + Buffer.BlockCopy(trustedComment, 0, globalMessage, signature.Signature.Length, trustedComment.Length); + return VerifyEd25519(publicKey.PublicKey, globalMessage, signature.GlobalSignature); + } + + private static ParsedPublicKey ParsePublicKey(string publicKeyBase64) + { + if (string.IsNullOrWhiteSpace(publicKeyBase64)) + throw new InvalidDataException("The Minisign public key is empty."); + + byte[] raw; + try + { + raw = Convert.FromBase64String(publicKeyBase64.Trim()); + } + catch (FormatException ex) + { + throw new InvalidDataException("The Minisign public key is not valid Base64.", ex); + } + + if (raw.Length != PublicKeyLength || raw[0] != (byte)'E' || raw[1] != (byte)'d') + throw new InvalidDataException("The Minisign public key has an unsupported format."); + + return new ParsedPublicKey(raw[2..10], raw[10..42]); + } + + private static ParsedSignature ParseSignature(string signatureText) + { + if (string.IsNullOrWhiteSpace(signatureText)) + throw new InvalidDataException("The Minisign signature is empty."); + + string[] lines = signatureText + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n') + .Split('\n', StringSplitOptions.RemoveEmptyEntries); + + if (lines.Length != 4) + throw new InvalidDataException("The Minisign signature must contain exactly four non-empty lines."); + if (!lines[0].StartsWith(UntrustedCommentPrefix, StringComparison.Ordinal)) + throw new InvalidDataException("The Minisign signature has an invalid untrusted comment."); + if (!lines[2].StartsWith(TrustedCommentPrefix, StringComparison.Ordinal)) + throw new InvalidDataException("The Minisign signature has an invalid trusted comment."); + + byte[] signatureRecord = DecodeBase64(lines[1], "signature"); + if (signatureRecord.Length != SignatureRecordLength + || signatureRecord[0] != (byte)'E' + || (signatureRecord[1] != (byte)'D' && signatureRecord[1] != (byte)'d')) + { + throw new InvalidDataException("The Minisign signature uses an unsupported algorithm."); + } + + byte[] globalSignature = DecodeBase64(lines[3], "global signature"); + if (globalSignature.Length != Ed25519SignatureLength) + throw new InvalidDataException("The Minisign global signature has an invalid length."); + + string trustedComment = lines[2].Substring(TrustedCommentPrefix.Length); + MinisignSignatureMode mode = signatureRecord[1] == (byte)'D' + ? MinisignSignatureMode.Prehashed + : MinisignSignatureMode.Legacy; + return new ParsedSignature( + signatureRecord[2..10], + signatureRecord[10..74], + trustedComment, + globalSignature, + mode); + } + + private static byte[] DecodeBase64(string value, string description) + { + try + { + return Convert.FromBase64String(value.Trim()); + } + catch (FormatException ex) + { + throw new InvalidDataException($"The Minisign {description} is not valid Base64.", ex); + } + } + + private static byte[] ComputeBlake2b512(string filePath) + { + var digest = new Blake2bDigest(512); + byte[] buffer = new byte[64 * 1024]; + using FileStream input = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + digest.BlockUpdate(buffer, 0, read); + } + + byte[] hash = new byte[Blake2bHashLength]; + digest.DoFinal(hash, 0); + return hash; + } + + private static bool VerifyEd25519(byte[] publicKey, byte[] message, byte[] signature) + { + var verifier = new Ed25519Signer(); + verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0)); + verifier.BlockUpdate(message, 0, message.Length); + return verifier.VerifySignature(signature); + } + + private static bool VerifyEd25519File(byte[] publicKey, string filePath, byte[] signature) + { + var verifier = new Ed25519Signer(); + verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0)); + + byte[] buffer = new byte[64 * 1024]; + using FileStream input = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + verifier.BlockUpdate(buffer, 0, read); + } + + return verifier.VerifySignature(signature); + } + + private enum MinisignSignatureMode + { + Legacy, + Prehashed, + } + + private sealed record ParsedPublicKey(byte[] KeyId, byte[] PublicKey); + private sealed record ParsedSignature( + byte[] KeyId, + byte[] Signature, + string TrustedComment, + byte[] GlobalSignature, + MinisignSignatureMode Mode); + } +} diff --git a/Shadowsocks.Windows/Controller/Service/PluginManager.cs b/Shadowsocks.Windows/Controller/Service/PluginManager.cs new file mode 100644 index 000000000..33b9a79e3 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/PluginManager.cs @@ -0,0 +1,1032 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Formats.Tar; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Core; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Service +{ + public sealed record PluginCatalogEntry( + string Id, + string DisplayName, + string Repository, + string WindowsAssetMarker, + string PreferredExecutableMarker = null); + + public sealed record InstalledPlugin( + string Id, + string DisplayName, + string ExecutablePath, + string Source, + bool CanAutoUpdate = false, + bool AutoUpdate = false, + string Repository = null, + string ReleaseTag = null, + string AssetName = null, + DateTimeOffset? LastUpdateCheckUtc = null); + + public sealed record PluginUpdateSummary( + int CheckedCount, + int UpdatedCount, + int SkippedInUseCount, + IReadOnlyList Errors); + + public static class PluginManager + { + private const string MetadataFileName = ".plugin.json"; + private const long MaxArchiveBytes = 256L * 1024 * 1024; + private const long MaxExtractedBytes = 512L * 1024 * 1024; + private const int MaxArchiveEntries = 4096; + private static readonly TimeSpan AutomaticCheckInterval = TimeSpan.FromHours(24); + private static readonly TimeSpan TransientDirectoryRetention = TimeSpan.FromDays(1); + private static readonly JsonSerializerOptions s_metadataWriteOptions = new() { WriteIndented = true }; + private static readonly HttpClient s_httpClient = CreateHttpClient(); + private static readonly object s_storageSync = new(); + private static readonly SemaphoreSlim s_updateSync = new(1, 1); + + internal static string PluginsDirectoryOverride { get; set; } + internal static HttpClient HttpClientOverride { get; set; } + + private static HttpClient Client => HttpClientOverride ?? s_httpClient; + private static string PluginsDirectory => string.IsNullOrWhiteSpace(PluginsDirectoryOverride) + ? AppStoragePaths.PluginsDirectory + : Path.GetFullPath(PluginsDirectoryOverride); + + public static IReadOnlyList Catalog { get; } = + [ + new("xray-plugin", "Xray Plugin", "teddysun/xray-plugin", "windows-amd64"), + new("v2ray-plugin", "V2Ray Plugin", "shadowsocks/v2ray-plugin", "windows-amd64"), + new("qtun", "QTun", "shadowsocks/qtun", "x86_64-pc-windows-msvc", "client"), + ]; + + public static IReadOnlyList GetInstalledPlugins() + { + lock (s_storageSync) + { + Directory.CreateDirectory(PluginsDirectory); + RecoverInterruptedPluginSwapsLocked(); + var result = new List(); + foreach (string directory in Directory.EnumerateDirectories(PluginsDirectory)) + { + if (Path.GetFileName(directory).StartsWith(".plugin-", StringComparison.OrdinalIgnoreCase)) + continue; + + PluginMetadata metadata = TryReadMetadata(directory); + if (metadata is null || string.IsNullOrWhiteSpace(metadata.EntryPoint)) + continue; + + string executablePath = Path.GetFullPath(Path.Combine(directory, metadata.EntryPoint)); + if (!IsPathInside(directory, executablePath) || !File.Exists(executablePath)) + continue; + + PluginCatalogEntry catalogEntry = GetCatalogEntryForMetadata(metadata); + bool canAutoUpdate = catalogEntry is not null; + bool autoUpdate = canAutoUpdate && metadata.AutoUpdate != false; + result.Add(new InstalledPlugin( + metadata.Id, + string.IsNullOrWhiteSpace(metadata.DisplayName) ? metadata.Id : metadata.DisplayName, + executablePath, + metadata.Source ?? "Manual archive", + canAutoUpdate, + autoUpdate, + catalogEntry?.Repository, + metadata.ReleaseTag, + metadata.AssetName, + metadata.LastUpdateCheckUtc)); + } + + return result.OrderBy(plugin => plugin.DisplayName, StringComparer.CurrentCultureIgnoreCase).ToArray(); + } + } + + public static string ResolveExecutable(string plugin) + { + if (string.IsNullOrWhiteSpace(plugin)) + return null; + + string id = plugin.Trim(); + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(id); + InstalledPlugin installed = GetInstalledPlugins().FirstOrDefault(candidate => + string.Equals(candidate.Id, id, StringComparison.OrdinalIgnoreCase) + || string.Equals(Path.GetFileNameWithoutExtension(candidate.ExecutablePath), fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); + return installed?.ExecutablePath; + } + + public static async Task InstallCatalogPluginAsync(PluginCatalogEntry entry, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(entry); + await s_updateSync.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + GitHubRelease release = await GetLatestReleaseAsync(entry.Repository, cancellationToken).ConfigureAwait(false); + GitHubAsset asset = SelectReleaseAsset(entry, release); + bool autoUpdate = GetInstalledPlugins() + .FirstOrDefault(plugin => string.Equals(plugin.Id, entry.Id, StringComparison.OrdinalIgnoreCase) && plugin.CanAutoUpdate) + ?.AutoUpdate ?? true; + + return await DownloadAndInstallCatalogReleaseAsync( + entry, + release, + asset, + autoUpdate, + requireExistingCatalogInstallation: false, + cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"The {entry.DisplayName} package was removed while it was being installed."); + } + finally + { + s_updateSync.Release(); + } + } + + public static async Task UpdateCatalogPluginsAsync( + IReadOnlyCollection pluginsInUse = null, + bool force = false, + CancellationToken cancellationToken = default) + { + await s_updateSync.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + CleanupStaleTransientDirectories(); + InstalledPlugin[] installedPlugins = GetInstalledPlugins().ToArray(); + var excluded = new HashSet( + (pluginsInUse ?? Array.Empty()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(GetPluginIdentity), + StringComparer.OrdinalIgnoreCase); + + int checkedCount = 0; + int updatedCount = 0; + int skippedInUseCount = 0; + var errors = new List(); + + foreach (InstalledPlugin installed in installedPlugins) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!installed.CanAutoUpdate || !installed.AutoUpdate) + continue; + + PluginCatalogEntry entry = FindCatalogEntry(installed.Id); + if (entry is null) + continue; + + if (excluded.Contains(GetPluginIdentity(installed.Id))) + { + skippedInUseCount++; + continue; + } + + DateTimeOffset nowUtc = DateTimeOffset.UtcNow; + if (!force && !IsAutomaticCheckDue(installed.LastUpdateCheckUtc, nowUtc)) + continue; + + PluginMetadata currentMetadata = GetMetadataForAutomaticUpdate(installed.Id, entry); + if (currentMetadata is null) + continue; + + checkedCount++; + try + { + GitHubRelease release = await GetLatestReleaseAsync(entry.Repository, cancellationToken).ConfigureAwait(false); + GitHubAsset asset = SelectReleaseAsset(entry, release); + if (!string.IsNullOrWhiteSpace(currentMetadata.ReleaseTag) + && string.Equals(currentMetadata.ReleaseTag, release.TagName, StringComparison.OrdinalIgnoreCase)) + { + UpdateObservedReleaseMetadata(installed.Id, entry, release.TagName, asset.Name, nowUtc); + continue; + } + + if (IsReleaseDowngrade(currentMetadata.ReleaseTag, release.TagName)) + { + UpdateLastSuccessfulCheck(installed.Id, entry, nowUtc); + continue; + } + + InstalledPlugin updated = await DownloadAndInstallCatalogReleaseAsync( + entry, + release, + asset, + autoUpdate: true, + requireExistingCatalogInstallation: true, + cancellationToken).ConfigureAwait(false); + if (updated is not null) + updatedCount++; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + errors.Add($"{installed.DisplayName}: {exception.Message}"); + } + } + + return new PluginUpdateSummary(checkedCount, updatedCount, skippedInUseCount, errors); + } + finally + { + s_updateSync.Release(); + } + } + + public static bool SetAutoUpdate(string pluginId, bool enabled) + { + if (string.IsNullOrWhiteSpace(pluginId)) + return false; + + lock (s_storageSync) + { + string directory = GetPluginDirectory(pluginId); + PluginMetadata metadata = TryReadMetadata(directory); + if (metadata is null || GetCatalogEntryForMetadata(metadata) is null) + return false; + + metadata.AutoUpdate = enabled; + WriteMetadata(directory, metadata); + return true; + } + } + + public static InstalledPlugin InstallManualArchive(string archivePath) + { + if (string.IsNullOrWhiteSpace(archivePath)) + throw new ArgumentException("Plugin archive path is required.", nameof(archivePath)); + + if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + return InstallArchive(archivePath, null, null, "Manual ZIP", "client", null, null, null, false, null, false); + + if (archivePath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) + || archivePath.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) + { + return InstallArchive(archivePath, null, null, "Manual TAR.GZ", "client", null, null, null, false, null, false); + } + + throw new InvalidDataException("Only ZIP and TAR.GZ plugin packages are supported."); + } + + public static InstalledPlugin InstallManualZip(string zipPath) + { + if (string.IsNullOrWhiteSpace(zipPath)) + throw new ArgumentException("ZIP path is required.", nameof(zipPath)); + if (!zipPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("Only ZIP plugin packages are supported."); + + return InstallManualArchive(zipPath); + } + + public static void Remove(string pluginId) + { + if (string.IsNullOrWhiteSpace(pluginId)) + return; + + lock (s_storageSync) + { + string directory = GetPluginDirectory(pluginId); + if (Directory.Exists(directory)) + Directory.Delete(directory, recursive: true); + } + } + + internal static bool IsAutomaticCheckDue(DateTimeOffset? lastCheckUtc, DateTimeOffset nowUtc) + { + if (!lastCheckUtc.HasValue || lastCheckUtc.Value > nowUtc) + return true; + + return nowUtc - lastCheckUtc.Value >= AutomaticCheckInterval; + } + + public static PluginCatalogEntry FindCatalogEntry(string pluginId) + { + if (string.IsNullOrWhiteSpace(pluginId)) + return null; + + string identity = GetPluginIdentity(pluginId); + return Catalog.FirstOrDefault(entry => + string.Equals(entry.Id, identity, StringComparison.OrdinalIgnoreCase) + || (!string.IsNullOrWhiteSpace(entry.PreferredExecutableMarker) + && string.Equals( + $"{entry.Id}-{entry.PreferredExecutableMarker}", + identity, + StringComparison.OrdinalIgnoreCase))); + } + + private static async Task DownloadAndInstallCatalogReleaseAsync( + PluginCatalogEntry entry, + GitHubRelease release, + GitHubAsset asset, + bool autoUpdate, + bool requireExistingCatalogInstallation, + CancellationToken cancellationToken) + { + string stagingRoot = AppStoragePaths.EnsureTempDirectory(Path.Combine(AppStoragePaths.TempUpdatesRoot, "Plugins")); + string archivePath = Path.Combine(stagingRoot, $"{entry.Id}-{Guid.NewGuid():N}{GetArchiveSuffix(asset.Name)}"); + try + { + using HttpResponseMessage response = await Client.GetAsync(asset.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + long? contentLength = response.Content.Headers.ContentLength; + if (contentLength is > MaxArchiveBytes) + throw new InvalidDataException($"Plugin archive is larger than the {MaxArchiveBytes / (1024 * 1024)} MiB safety limit."); + + await using (Stream source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + await using (FileStream target = new(archivePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + await CopyToWithLimitAsync(source, target, MaxArchiveBytes, cancellationToken).ConfigureAwait(false); + } + + VerifyReleaseAssetDigest(archivePath, asset); + + return InstallArchive( + archivePath, + entry.Id, + entry.DisplayName, + $"GitHub: {entry.Repository}", + entry.PreferredExecutableMarker, + entry, + release.TagName, + asset.Name, + autoUpdate, + DateTimeOffset.UtcNow, + requireExistingCatalogInstallation, + cancellationToken); + } + finally + { + TryDeleteFile(archivePath); + } + } + + private static InstalledPlugin InstallArchive( + string archivePath, + string pluginId, + string displayName, + string source, + string preferredExecutableMarker, + PluginCatalogEntry catalogEntry, + string releaseTag, + string assetName, + bool autoUpdate, + DateTimeOffset? lastUpdateCheckUtc, + bool requireExistingCatalogInstallation, + CancellationToken cancellationToken = default) + { + if (!File.Exists(archivePath)) + throw new FileNotFoundException("Plugin archive was not found.", archivePath); + if (new FileInfo(archivePath).Length > MaxArchiveBytes) + throw new InvalidDataException($"Plugin archive is larger than the {MaxArchiveBytes / (1024 * 1024)} MiB safety limit."); + + CleanupStaleTransientDirectories(); + Directory.CreateDirectory(PluginsDirectory); + string staging = Path.Combine(PluginsDirectory, $".plugin-stage-{Guid.NewGuid():N}"); + string backup = null; + + try + { + Directory.CreateDirectory(staging); + if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + ExtractZipSafely(archivePath, staging, cancellationToken); + } + else if (archivePath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) + || archivePath.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) + { + ExtractTarGz(archivePath, staging, cancellationToken); + } + else + { + throw new InvalidDataException("Unsupported plugin archive format."); + } + + string executable = SelectExecutable(staging, preferredExecutableMarker); + string safeId = SanitizeId(pluginId ?? InferPluginId(executable)); + string finalDisplayName = string.IsNullOrWhiteSpace(displayName) ? safeId : displayName; + string destination = GetPluginDirectory(safeId); + string relativeEntryPoint = Path.GetRelativePath(staging, executable); + + lock (s_storageSync) + { + if (requireExistingCatalogInstallation) + { + PluginMetadata existingMetadata = TryReadMetadata(destination); + PluginCatalogEntry existingCatalogEntry = GetCatalogEntryForMetadata(existingMetadata); + if (existingMetadata is null + || existingCatalogEntry is null + || !string.Equals(existingCatalogEntry.Repository, catalogEntry?.Repository, StringComparison.OrdinalIgnoreCase) + || existingMetadata.AutoUpdate == false) + { + TryDeleteDirectory(staging); + return null; + } + } + + WriteMetadata(staging, new PluginMetadata + { + Id = safeId, + DisplayName = finalDisplayName, + EntryPoint = relativeEntryPoint, + Source = source, + Repository = catalogEntry?.Repository, + ReleaseTag = releaseTag, + AssetName = assetName, + AutoUpdate = catalogEntry is null ? null : autoUpdate, + LastUpdateCheckUtc = lastUpdateCheckUtc, + }); + + if (Directory.Exists(destination)) + { + backup = Path.Combine(PluginsDirectory, $".plugin-backup-{safeId}-{Guid.NewGuid():N}"); + Directory.Move(destination, backup); + } + + try + { + Directory.Move(staging, destination); + } + catch + { + if (backup is not null && Directory.Exists(backup) && !Directory.Exists(destination)) + Directory.Move(backup, destination); + throw; + } + } + + if (backup is not null) + TryDeleteDirectory(backup); + + string finalExecutable = Path.Combine(destination, relativeEntryPoint); + return new InstalledPlugin( + safeId, + finalDisplayName, + finalExecutable, + source, + catalogEntry is not null, + catalogEntry is not null && autoUpdate, + catalogEntry?.Repository, + releaseTag, + assetName, + lastUpdateCheckUtc); + } + catch + { + TryDeleteDirectory(staging); + throw; + } + } + + private static PluginMetadata GetMetadataForAutomaticUpdate(string pluginId, PluginCatalogEntry entry) + { + lock (s_storageSync) + { + string directory = GetPluginDirectory(pluginId); + PluginMetadata metadata = TryReadMetadata(directory); + PluginCatalogEntry installedEntry = GetCatalogEntryForMetadata(metadata); + if (metadata is null + || installedEntry is null + || !string.Equals(installedEntry.Repository, entry.Repository, StringComparison.OrdinalIgnoreCase) + || metadata.AutoUpdate == false) + { + return null; + } + + return CloneMetadata(metadata); + } + } + + private static bool IsReleaseDowngrade(string installedTag, string candidateTag) + { + return UpdateChecker.TryParseReleaseVersion(installedTag, out Version installedVersion) + && UpdateChecker.TryParseReleaseVersion(candidateTag, out Version candidateVersion) + && candidateVersion < installedVersion; + } + + private static void UpdateLastSuccessfulCheck(string pluginId, PluginCatalogEntry entry, DateTimeOffset nowUtc) + { + lock (s_storageSync) + { + string directory = GetPluginDirectory(pluginId); + PluginMetadata metadata = TryReadMetadata(directory); + PluginCatalogEntry installedEntry = GetCatalogEntryForMetadata(metadata); + if (metadata is null + || installedEntry is null + || !string.Equals(installedEntry.Repository, entry.Repository, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + metadata.LastUpdateCheckUtc = nowUtc; + WriteMetadata(directory, metadata); + } + } + + private static void UpdateObservedReleaseMetadata( + string pluginId, + PluginCatalogEntry entry, + string releaseTag, + string assetName, + DateTimeOffset nowUtc) + { + lock (s_storageSync) + { + string directory = GetPluginDirectory(pluginId); + PluginMetadata metadata = TryReadMetadata(directory); + PluginCatalogEntry installedEntry = GetCatalogEntryForMetadata(metadata); + if (metadata is null + || installedEntry is null + || !string.Equals(installedEntry.Repository, entry.Repository, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + metadata.Repository = entry.Repository; + metadata.ReleaseTag = releaseTag; + metadata.AssetName = assetName; + metadata.LastUpdateCheckUtc = nowUtc; + WriteMetadata(directory, metadata); + } + } + + private static PluginMetadata CloneMetadata(PluginMetadata metadata) + => new() + { + Id = metadata.Id, + DisplayName = metadata.DisplayName, + EntryPoint = metadata.EntryPoint, + Source = metadata.Source, + Repository = metadata.Repository, + ReleaseTag = metadata.ReleaseTag, + AssetName = metadata.AssetName, + AutoUpdate = metadata.AutoUpdate, + LastUpdateCheckUtc = metadata.LastUpdateCheckUtc, + }; + + private static PluginCatalogEntry GetCatalogEntryForMetadata(PluginMetadata metadata) + { + if (metadata is null || string.IsNullOrWhiteSpace(metadata.Id)) + return null; + + PluginCatalogEntry entry = FindCatalogEntry(metadata.Id); + if (entry is null) + return null; + + bool trustedRepository = string.Equals(metadata.Repository, entry.Repository, StringComparison.OrdinalIgnoreCase); + bool legacyCatalogSource = string.Equals(metadata.Source, $"GitHub: {entry.Repository}", StringComparison.OrdinalIgnoreCase); + return trustedRepository || legacyCatalogSource ? entry : null; + } + + private static string InferPluginId(string executablePath) + { + string fileName = Path.GetFileNameWithoutExtension(executablePath); + if (fileName.Contains("xray-plugin", StringComparison.OrdinalIgnoreCase)) + return "xray-plugin"; + if (fileName.Contains("v2ray-plugin", StringComparison.OrdinalIgnoreCase)) + return "v2ray-plugin"; + if (fileName.Contains("qtun", StringComparison.OrdinalIgnoreCase)) + return "qtun"; + return fileName; + } + + private static void ExtractTarGz(string archivePath, string destination, CancellationToken cancellationToken) + { + string destinationRoot = Path.GetFullPath(destination).TrimEnd(Path.DirectorySeparatorChar); + string root = destinationRoot + Path.DirectorySeparatorChar; + using FileStream source = File.OpenRead(archivePath); + using var gzip = new GZipStream(source, CompressionMode.Decompress); + using var reader = new TarReader(gzip, leaveOpen: false); + + int entryCount = 0; + long extractedBytes = 0; + byte[] buffer = new byte[81920]; + TarEntry entry; + while ((entry = reader.GetNextEntry()) != null) + { + cancellationToken.ThrowIfCancellationRequested(); + if (++entryCount > MaxArchiveEntries) + throw new InvalidDataException($"The TAR.GZ contains more than {MaxArchiveEntries} entries."); + if (string.IsNullOrWhiteSpace(entry.Name)) + continue; + + string normalizedName = entry.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + ValidateArchiveEntryName(normalizedName, "TAR.GZ"); + string target = Path.GetFullPath(Path.Combine(destination, normalizedName)); + if (!target.Equals(destinationRoot, StringComparison.OrdinalIgnoreCase) + && !target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("The TAR.GZ contains an unsafe path."); + } + + if (entry.EntryType == TarEntryType.Directory || entry.EntryType == TarEntryType.DirectoryList) + { + Directory.CreateDirectory(target); + continue; + } + + if (entry.EntryType != TarEntryType.RegularFile + && entry.EntryType != TarEntryType.V7RegularFile + && entry.EntryType != TarEntryType.ContiguousFile) + { + throw new InvalidDataException("The TAR.GZ contains an unsupported link or special entry."); + } + + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + using FileStream output = new(target, FileMode.Create, FileAccess.Write, FileShare.None); + CopyStreamWithLimit(entry.DataStream, output, buffer, ref extractedBytes, "TAR.GZ", cancellationToken); + } + } + + private static void ExtractZipSafely(string zipPath, string destination, CancellationToken cancellationToken) + { + string destinationRoot = Path.GetFullPath(destination).TrimEnd(Path.DirectorySeparatorChar); + string root = destinationRoot + Path.DirectorySeparatorChar; + using ZipArchive archive = ZipFile.OpenRead(zipPath); + if (archive.Entries.Count > MaxArchiveEntries) + throw new InvalidDataException($"The ZIP contains more than {MaxArchiveEntries} entries."); + + long declaredExtractedBytes = 0; + long extractedBytes = 0; + byte[] buffer = new byte[81920]; + foreach (ZipArchiveEntry entry in archive.Entries) + { + cancellationToken.ThrowIfCancellationRequested(); + if (entry.Length > MaxExtractedBytes - declaredExtractedBytes) + throw new InvalidDataException($"The ZIP expands beyond the {MaxExtractedBytes / (1024 * 1024)} MiB safety limit."); + declaredExtractedBytes += entry.Length; + + ValidateArchiveEntryName(entry.FullName, "ZIP"); + string target = Path.GetFullPath(Path.Combine(destination, entry.FullName)); + if (!target.Equals(destinationRoot, StringComparison.OrdinalIgnoreCase) + && !target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("The ZIP contains an unsafe path."); + } + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(target); + continue; + } + + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + using Stream input = entry.Open(); + using FileStream output = new(target, FileMode.Create, FileAccess.Write, FileShare.None); + CopyStreamWithLimit(input, output, buffer, ref extractedBytes, "ZIP", cancellationToken); + } + } + + private static void ValidateArchiveEntryName(string entryName, string archiveType) + { + if (string.IsNullOrWhiteSpace(entryName) || Path.IsPathRooted(entryName) || entryName.Contains(':')) + throw new InvalidDataException($"The {archiveType} contains an unsafe path."); + } + + private static void CopyStreamWithLimit( + Stream source, + FileStream destination, + byte[] buffer, + ref long extractedBytes, + string archiveType, + CancellationToken cancellationToken) + { + if (source is null) + return; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead == 0) + return; + + extractedBytes = AccumulateExtractedBytes(extractedBytes, bytesRead, archiveType); + destination.Write(buffer, 0, bytesRead); + } + } + + internal static long AccumulateExtractedBytes(long extractedBytes, int bytesRead, string archiveType) + { + if (bytesRead > MaxExtractedBytes - extractedBytes) + throw new InvalidDataException($"The {archiveType} expands beyond the {MaxExtractedBytes / (1024 * 1024)} MiB safety limit."); + + return extractedBytes + bytesRead; + } + + private static string SelectExecutable(string root, string preferredMarker) + { + string[] executables = Directory.EnumerateFiles(root, "*.exe", SearchOption.AllDirectories).ToArray(); + if (executables.Length == 0) + throw new InvalidDataException("The plugin archive does not contain a Windows executable."); + if (executables.Length == 1) + return executables[0]; + + if (!string.IsNullOrWhiteSpace(preferredMarker)) + { + string[] preferred = executables.Where(path => + Path.GetFileName(path).Contains(preferredMarker, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (preferred.Length == 1) + return preferred[0]; + } + + string[] clientExecutables = executables.Where(path => + Path.GetFileName(path).Contains("client", StringComparison.OrdinalIgnoreCase) + && !Path.GetFileName(path).Contains("server", StringComparison.OrdinalIgnoreCase)).ToArray(); + if (clientExecutables.Length == 1) + return clientExecutables[0]; + + throw new InvalidDataException("The plugin archive contains multiple executables and the client executable could not be selected automatically."); + } + + private static GitHubAsset SelectReleaseAsset(PluginCatalogEntry entry, GitHubRelease release) + { + GitHubAsset asset = release.Assets.FirstOrDefault(candidate => + candidate.Name.Contains(entry.WindowsAssetMarker, StringComparison.OrdinalIgnoreCase) + && IsSupportedArchive(candidate.Name)); + if (asset is null || string.IsNullOrWhiteSpace(asset.DownloadUrl)) + throw new InvalidOperationException($"No Windows x64 archive was found in the latest {entry.Repository} release."); + return asset; + } + + private static bool IsSupportedArchive(string fileName) + => fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase); + + private static string GetArchiveSuffix(string fileName) + { + if (fileName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) + return ".tar.gz"; + if (fileName.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) + return ".tgz"; + if (fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + return ".zip"; + throw new InvalidDataException("Unsupported plugin archive format."); + } + + private static async Task GetLatestReleaseAsync(string repository, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await Client.GetAsync($"https://api.github.com/repos/{repository}/releases/latest", cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + GitHubRelease release = await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false) + ?? throw new InvalidDataException("GitHub returned an empty release response."); + if (string.IsNullOrWhiteSpace(release.TagName)) + throw new InvalidDataException("GitHub release response does not contain a release tag."); + return release; + } + + private static async Task CopyToWithLimitAsync( + Stream source, + FileStream destination, + long maximumBytes, + CancellationToken cancellationToken) + { + byte[] buffer = new byte[81920]; + long totalBytes = 0; + while (true) + { + int bytesRead = await source.ReadAsync(buffer.AsMemory(), cancellationToken).ConfigureAwait(false); + if (bytesRead == 0) + return; + + totalBytes += bytesRead; + if (totalBytes > maximumBytes) + throw new InvalidDataException($"Plugin archive is larger than the {maximumBytes / (1024 * 1024)} MiB safety limit."); + + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + } + } + + private static void VerifyReleaseAssetDigest(string archivePath, GitHubAsset asset) + { + if (string.IsNullOrWhiteSpace(asset.Digest)) + return; + + const string Prefix = "sha256:"; + if (!asset.Digest.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("GitHub returned an unsupported plugin asset digest algorithm."); + + string expected = asset.Digest[Prefix.Length..].Trim(); + if (expected.Length != 64 || expected.Any(character => !Uri.IsHexDigit(character))) + throw new InvalidDataException("GitHub returned an invalid SHA-256 plugin asset digest."); + + using FileStream stream = File.OpenRead(archivePath); + string actual = Convert.ToHexString(SHA256.HashData(stream)); + if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("The downloaded plugin archive failed GitHub SHA-256 verification."); + } + + private static void CleanupStaleTransientDirectories() + { + lock (s_storageSync) + { + if (!Directory.Exists(PluginsDirectory)) + return; + + RecoverInterruptedPluginSwapsLocked(); + DateTime cutoffUtc = DateTime.UtcNow - TransientDirectoryRetention; + foreach (string directory in Directory.EnumerateDirectories(PluginsDirectory, ".plugin-*", SearchOption.TopDirectoryOnly)) + { + try + { + if (ShouldPreserveRecoverableBackupLocked(directory)) + continue; + + if (Directory.GetLastWriteTimeUtc(directory) <= cutoffUtc) + Directory.Delete(directory, recursive: true); + } + catch + { + // Best effort: an old plugin process can still hold a transient file open. + } + } + } + } + + private static bool ShouldPreserveRecoverableBackupLocked(string directory) + { + if (!Path.GetFileName(directory).StartsWith(".plugin-backup-", StringComparison.OrdinalIgnoreCase)) + return false; + + PluginMetadata metadata = TryReadMetadata(directory); + if (metadata is null || string.IsNullOrWhiteSpace(metadata.Id)) + return false; + + try + { + return !Directory.Exists(GetPluginDirectory(metadata.Id)); + } + catch (InvalidDataException) + { + return false; + } + } + + private static void RecoverInterruptedPluginSwapsLocked() + { + foreach (string backupDirectory in Directory.EnumerateDirectories(PluginsDirectory, ".plugin-backup-*", SearchOption.TopDirectoryOnly) + .OrderByDescending(Directory.GetLastWriteTimeUtc)) + { + PluginMetadata metadata = TryReadMetadata(backupDirectory); + if (metadata is null || string.IsNullOrWhiteSpace(metadata.Id)) + continue; + + string destination; + try + { + destination = GetPluginDirectory(metadata.Id); + } + catch (InvalidDataException) + { + continue; + } + + if (Directory.Exists(destination)) + continue; + + try + { + Directory.Move(backupDirectory, destination); + } + catch (IOException) + { + // Best effort. A subsequent storage read or maintenance pass can retry. + } + catch (UnauthorizedAccessException) + { + // Best effort. Preserve the backup rather than deleting recoverable state. + } + } + } + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient { Timeout = TimeSpan.FromMinutes(2) }; + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Shadowsocks-Reborn", ApplicationInfo.Version)); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + return client; + } + + private static PluginMetadata TryReadMetadata(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + return null; + + try + { + string path = Path.Combine(directory, MetadataFileName); + return File.Exists(path) + ? JsonSerializer.Deserialize(File.ReadAllText(path)) + : null; + } + catch + { + return null; + } + } + + private static void WriteMetadata(string directory, PluginMetadata metadata) + { + Directory.CreateDirectory(directory); + string path = Path.Combine(directory, MetadataFileName); + string temporaryPath = Path.Combine(directory, $"{MetadataFileName}.tmp-{Guid.NewGuid():N}"); + try + { + string json = JsonSerializer.Serialize(metadata, s_metadataWriteOptions); + File.WriteAllText(temporaryPath, json); + File.Move(temporaryPath, path, overwrite: true); + } + finally + { + TryDeleteFile(temporaryPath); + } + } + + private static string GetPluginDirectory(string pluginId) + { + string root = Path.GetFullPath(PluginsDirectory); + string directory = Path.GetFullPath(Path.Combine(root, SanitizeId(pluginId))); + if (!IsPathInside(root, directory)) + throw new InvalidDataException("Plugin id resolves outside the plugin storage root."); + return directory; + } + + private static string GetPluginIdentity(string plugin) + { + string value = plugin?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + return Path.GetFileNameWithoutExtension(value); + } + + private static string SanitizeId(string value) + { + string id = new(value.Trim().Select(character => + char.IsLetterOrDigit(character) || character is '-' or '_' or '.' ? character : '-').ToArray()); + id = id.Trim('-', '.', '_'); + if (string.IsNullOrWhiteSpace(id)) + id = "plugin"; + return id; + } + + private static bool IsPathInside(string root, string path) + { + string normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + string normalizedPath = Path.GetFullPath(path); + return normalizedPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase); + } + + private static void TryDeleteFile(string path) + { + try { if (File.Exists(path)) File.Delete(path); } catch { } + } + + private static void TryDeleteDirectory(string path) + { + try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); } catch { } + } + + private sealed class PluginMetadata + { + public string Id { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string EntryPoint { get; set; } = string.Empty; + public string Source { get; set; } + public string Repository { get; set; } + public string ReleaseTag { get; set; } + public string AssetName { get; set; } + public bool? AutoUpdate { get; set; } + public DateTimeOffset? LastUpdateCheckUtc { get; set; } + } + + private sealed class GitHubRelease + { + [JsonPropertyName("tag_name")] + public string TagName { get; set; } = string.Empty; + + [JsonPropertyName("assets")] + public List Assets { get; set; } = []; + } + + private sealed class GitHubAsset + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; + + [JsonPropertyName("digest")] + public string Digest { get; set; } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/SelfUpdater.cs b/Shadowsocks.Windows/Controller/Service/SelfUpdater.cs new file mode 100644 index 000000000..2178c13c5 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/SelfUpdater.cs @@ -0,0 +1,850 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security; +using System.Threading; +using NLog; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller.Service +{ + /// + /// Implements the single-file updater handoff without shipping a separate updater binary. + /// The downloaded new Shadowsocks.exe is staged as Shadowsocks.Update.exe, replaces the + /// old executable after it exits, starts the installed new copy, and the installed copy + /// then removes the temporary updater transaction. + /// + public static class SelfUpdater + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static int updateHandoffStarted; + + internal const string UpdateSwitch = "--update"; + internal const string CleanupSwitch = "--update-cleanup"; + internal const string TargetOption = "--update-target"; + internal const string WaitPidOption = "--update-wait-pid"; + internal const string TransactionOption = "--update-transaction"; + internal const string BackupOption = "--update-backup"; + internal const string UpdaterPidOption = "--update-updater-pid"; + internal const string PayloadSha256Option = "--update-sha256"; + internal const string ResumeHiddenSwitch = "--update-resume-hidden"; + internal const string ResumeVisibleSwitch = "--update-resume-visible"; + internal const string TemporaryUpdaterFileName = "Shadowsocks.Update.exe"; + + private const int ProcessExitTimeoutMilliseconds = 60000; + private const int CleanupRetryCount = 20; + private const int CleanupRetryDelayMilliseconds = 250; + + public static string UpdatesRoot => Path.Combine( + Path.GetTempPath(), + "Shadowsocks", + "Updates"); + + /// + /// Executes the internal updater process mode before WinUI/AppInstance initialization. + /// + public static bool TryRunUpdaterMode(IReadOnlyList arguments, out int exitCode) + { + exitCode = 0; + if (!ContainsSwitch(arguments, UpdateSwitch)) + { + return false; + } + + try + { + string targetPath = GetRequiredOption(arguments, TargetOption); + string transactionDirectory = GetRequiredOption(arguments, TransactionOption); + int waitPid = ParseRequiredPid(arguments, WaitPidOption); + string expectedPayloadSha256 = GetRequiredOption(arguments, PayloadSha256Option); + bool resumeHidden = ContainsSwitch(arguments, ResumeHiddenSwitch); + bool resumeVisible = !resumeHidden && ContainsSwitch(arguments, ResumeVisibleSwitch); + exitCode = RunUpdater(targetPath, transactionDirectory, waitPid, expectedPayloadSha256, resumeHidden, resumeVisible); + } + catch (Exception exception) + { + try + { + Logger.Error(exception, "Application self-update failed in updater mode."); + } + catch + { + } + + exitCode = 20; + } + + return true; + } + + /// + /// Runs in the installed new copy. It waits for the temporary updater to exit and then + /// removes the updater transaction and rollback copy before normal application startup. + /// Cleanup is best-effort: a successful update must not be made unusable by antivirus + /// or delayed file-handle release. + /// + public static void CleanupCompletedUpdate(IReadOnlyList arguments) + { + if (!ContainsSwitch(arguments, CleanupSwitch)) + { + return; + } + + string transactionDirectory = GetOptionalOption(arguments, TransactionOption); + string backupPath = GetOptionalOption(arguments, BackupOption); + int updaterPid = ParseOptionalPid(arguments, UpdaterPidOption); + + if (updaterPid > 0 && updaterPid != Environment.ProcessId) + { + WaitForProcessExit(updaterPid, ProcessExitTimeoutMilliseconds); + } + + if (!string.IsNullOrWhiteSpace(backupPath) && IsCanonicalBackupPath(backupPath)) + { + TryDeleteFileWithRetry(backupPath); + } + + if (!string.IsNullOrWhiteSpace(transactionDirectory)) + { + TryDeleteDirectoryWithRetry(transactionDirectory); + } + + CleanupStaleTransactions(TimeSpan.FromDays(7)); + } + + /// + /// Removes updater-only switches before the normal application runtime stores or forwards + /// its command line. + /// + public static string[] RemoveInternalArguments(IReadOnlyList arguments) + { + if (arguments == null || arguments.Count == 0) + { + return Array.Empty(); + } + + var result = new List(arguments.Count); + for (int index = 0; index < arguments.Count; index++) + { + string argument = arguments[index] ?? string.Empty; + if (argument.Equals(UpdateSwitch, StringComparison.OrdinalIgnoreCase) + || argument.Equals(CleanupSwitch, StringComparison.OrdinalIgnoreCase) + || argument.Equals(ResumeHiddenSwitch, StringComparison.OrdinalIgnoreCase) + || argument.Equals(ResumeVisibleSwitch, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (IsInternalOption(argument)) + { + if (!argument.Contains('=') && index + 1 < arguments.Count) + { + index++; + } + continue; + } + + result.Add(argument); + } + + return result.ToArray(); + } + + public static string CreateTransactionDirectory() + { + Directory.CreateDirectory(UpdatesRoot); + CleanupStaleTransactions(TimeSpan.FromDays(7)); + + string directory = Path.Combine( + UpdatesRoot, + $"{DateTime.UtcNow:yyyyMMddHHmmss}-{Environment.ProcessId}-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + return directory; + } + + public static string GetTemporaryUpdaterPath(string transactionDirectory) + { + if (string.IsNullOrWhiteSpace(transactionDirectory)) + { + throw new ArgumentException("Update transaction directory is required.", nameof(transactionDirectory)); + } + + return Path.Combine(Path.GetFullPath(transactionDirectory), TemporaryUpdaterFileName); + } + + /// + /// Starts the staged new executable in updater mode. The caller must shut down the current + /// application only after this method returns successfully. + /// + public static Process LaunchStagedUpdater( + string stagedUpdaterPath, + string targetExecutablePath, + string transactionDirectory, + string expectedPayloadSha256) + { + if (Interlocked.CompareExchange(ref updateHandoffStarted, 1, 0) != 0) + { + throw new InvalidOperationException("An application update handoff is already in progress."); + } + + try + { + string updaterPath = Path.GetFullPath(stagedUpdaterPath ?? string.Empty); + string targetPath = Path.GetFullPath(targetExecutablePath ?? string.Empty); + string transactionPath = Path.GetFullPath(transactionDirectory ?? string.Empty); + + ValidateExecutablePath(updaterPath, nameof(stagedUpdaterPath)); + ValidateTargetPath(targetPath); + ValidateTransactionPath(transactionPath, updaterPath); + string normalizedPayloadSha256 = NormalizeSha256(expectedPayloadSha256); + + // Keep the verified staged executable read-only/non-deletable across Process.Start, + // including the UAC prompt. This closes the verify-to-elevate TOCTOU window for + // a payload staged below the current user's system-temp directory. + using FileStream stagedReadLock = new(updaterPath, FileMode.Open, FileAccess.Read, FileShare.Read); + VerifySha256(stagedReadLock, normalizedPayloadSha256); + + string targetDirectory = Path.GetDirectoryName(targetPath) + ?? throw new InvalidOperationException("The target executable directory is unavailable."); + + bool requiresElevation = !CanWriteDirectory(targetDirectory); + var startInfo = new ProcessStartInfo + { + FileName = updaterPath, + WorkingDirectory = transactionPath, + UseShellExecute = requiresElevation, + }; + if (requiresElevation) + { + startInfo.Verb = "runas"; + } + + startInfo.ArgumentList.Add(UpdateSwitch); + startInfo.ArgumentList.Add(TargetOption); + startInfo.ArgumentList.Add(targetPath); + startInfo.ArgumentList.Add(WaitPidOption); + startInfo.ArgumentList.Add(Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)); + startInfo.ArgumentList.Add(TransactionOption); + startInfo.ArgumentList.Add(transactionPath); + startInfo.ArgumentList.Add(PayloadSha256Option); + startInfo.ArgumentList.Add(normalizedPayloadSha256); + bool resumeHidden; + bool resumeVisible; + if (AutoStartup.TryGetTrackedWindowVisibility(out bool windowVisible)) + { + resumeVisible = windowVisible; + resumeHidden = !windowVisible; + } + else + { + resumeHidden = RuntimeEnvironment.Arguments.Any(argument => + string.Equals(argument, AutoStartup.StartupHiddenOption, StringComparison.OrdinalIgnoreCase)); + resumeVisible = !resumeHidden && RuntimeEnvironment.Arguments.Any(argument => + string.Equals(argument, AutoStartup.StartupVisibleOption, StringComparison.OrdinalIgnoreCase)); + } + + if (resumeHidden) + { + startInfo.ArgumentList.Add(ResumeHiddenSwitch); + } + else if (resumeVisible) + { + startInfo.ArgumentList.Add(ResumeVisibleSwitch); + } + + Process process = Process.Start(startInfo) + ?? throw new InvalidOperationException("The temporary update process could not be started."); + + Logger.Info( + "Started application updater process {0} for {1}{2}.", + process.Id, + targetPath, + requiresElevation ? " with elevation" : string.Empty); + return process; + } + catch + { + Interlocked.Exchange(ref updateHandoffStarted, 0); + throw; + } + } + + internal static bool IsCanonicalTransactionDirectory(string transactionDirectory) + { + if (string.IsNullOrWhiteSpace(transactionDirectory)) + { + return false; + } + + string root = EnsureTrailingSeparator(Path.GetFullPath(UpdatesRoot)); + string candidate = EnsureTrailingSeparator(Path.GetFullPath(transactionDirectory)); + return candidate.StartsWith(root, StringComparison.OrdinalIgnoreCase) + && !string.Equals(candidate, root, StringComparison.OrdinalIgnoreCase); + } + + internal static void CleanupStaleTransactions(TimeSpan retention) + { + try + { + if (!Directory.Exists(UpdatesRoot)) + { + return; + } + + DateTime cutoffUtc = DateTime.UtcNow - retention; + foreach (string directory in Directory.EnumerateDirectories(UpdatesRoot)) + { + try + { + if (Directory.GetLastWriteTimeUtc(directory) >= cutoffUtc) + { + continue; + } + + Directory.Delete(directory, recursive: true); + } + catch + { + } + } + } + catch + { + } + } + + private static int RunUpdater( + string targetPath, + string transactionDirectory, + int waitPid, + string expectedPayloadSha256, + bool resumeHidden, + bool resumeVisible) + { + string updaterPath = Environment.ProcessPath + ?? Process.GetCurrentProcess().MainModule?.FileName + ?? throw new InvalidOperationException("Unable to resolve the updater executable path."); + + updaterPath = Path.GetFullPath(updaterPath); + targetPath = Path.GetFullPath(targetPath); + transactionDirectory = Path.GetFullPath(transactionDirectory); + + ValidateExecutablePath(updaterPath, "updaterPath"); + ValidateTargetPath(targetPath); + ValidateTransactionPath(transactionDirectory, updaterPath); + using (FileStream updaterStream = new(updaterPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + VerifySha256(updaterStream, expectedPayloadSha256); + } + + if (waitPid == Environment.ProcessId) + { + throw new InvalidOperationException("Updater cannot wait for its own process ID."); + } + + if (!WaitForProcessExit(waitPid, ProcessExitTimeoutMilliseconds)) + { + throw new TimeoutException($"The previous Shadowsocks process {waitPid} did not exit in time."); + } + + string targetDirectory = Path.GetDirectoryName(targetPath) + ?? throw new InvalidOperationException("The target executable directory is unavailable."); + Directory.CreateDirectory(targetDirectory); + + string transactionId = Path.GetFileName(transactionDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + string incomingPath = Path.Combine(targetDirectory, $".{Path.GetFileName(targetPath)}.{transactionId}.update-new"); + string backupPath = Path.Combine(targetDirectory, $".{Path.GetFileName(targetPath)}.{transactionId}.update-backup"); + + TryDeleteFile(incomingPath); + TryDeleteFile(backupPath); + + bool hadOriginal = File.Exists(targetPath); + try + { + if (hadOriginal) + { + EnsureStrictlyNewerReplacement(updaterPath, targetPath); + } + + File.Copy(updaterPath, incomingPath, overwrite: true); + EnsureFilesMatch(updaterPath, incomingPath); + + if (hadOriginal) + { + // Re-read the target immediately before replacement as a second downgrade + // barrier in case another process changed the installed executable after + // the first validation. + EnsureStrictlyNewerReplacement(updaterPath, targetPath); + try + { + File.Replace(incomingPath, targetPath, backupPath, ignoreMetadataErrors: true); + } + catch (PlatformNotSupportedException) + { + ReplaceWithMoveFallback(incomingPath, targetPath, backupPath); + } + catch (IOException) + { + ReplaceWithMoveFallback(incomingPath, targetPath, backupPath); + } + } + else + { + File.Move(incomingPath, targetPath); + } + + EnsureFilesMatch(updaterPath, targetPath); + + var startInfo = new ProcessStartInfo + { + FileName = targetPath, + WorkingDirectory = targetDirectory, + UseShellExecute = false, + }; + startInfo.ArgumentList.Add(CleanupSwitch); + startInfo.ArgumentList.Add(TransactionOption); + startInfo.ArgumentList.Add(transactionDirectory); + startInfo.ArgumentList.Add(BackupOption); + startInfo.ArgumentList.Add(backupPath); + startInfo.ArgumentList.Add(UpdaterPidOption); + startInfo.ArgumentList.Add(Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)); + string resumeStartupOption = ResolveResumeStartupOption(resumeHidden, resumeVisible); + if (!string.IsNullOrEmpty(resumeStartupOption)) + { + startInfo.ArgumentList.Add(resumeStartupOption); + } + + if (Process.Start(startInfo) is null) + { + throw new InvalidOperationException("The installed updated executable could not be started."); + } + + return 0; + } + catch + { + TryDeleteFile(incomingPath); + if (hadOriginal) + { + try + { + // File.Replace/fallback may fail before the new payload is fully installed. + // If a rollback copy exists, restore it; otherwise the untouched target is + // still the previous executable and can simply be started again. + if (File.Exists(backupPath)) + { + File.Copy(backupPath, targetPath, overwrite: true); + } + + if (File.Exists(targetPath)) + { + TryStartRollback(targetPath, targetDirectory, resumeHidden, resumeVisible); + } + } + catch (Exception rollbackException) + { + try + { + Logger.Error(rollbackException, "Failed to restore the previous executable after an update failure."); + } + catch + { + } + } + } + + throw; + } + } + + internal static bool IsStrictlyNewerReplacement(Version payloadVersion, Version installedVersion) + { + ArgumentNullException.ThrowIfNull(payloadVersion); + ArgumentNullException.ThrowIfNull(installedVersion); + return payloadVersion.CompareTo(installedVersion) > 0; + } + + private static void EnsureStrictlyNewerReplacement(string payloadPath, string installedPath) + { + Version payloadVersion = GetExecutableVersion(payloadPath); + Version installedVersion = GetExecutableVersion(installedPath); + if (!IsStrictlyNewerReplacement(payloadVersion, installedVersion)) + { + throw new InvalidDataException( + $"Refusing to replace installed version {installedVersion} with non-newer update payload {payloadVersion}."); + } + } + + private static Version GetExecutableVersion(string executablePath) + { + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(executablePath); + return new Version( + Math.Max(0, versionInfo.FileMajorPart), + Math.Max(0, versionInfo.FileMinorPart), + Math.Max(0, versionInfo.FileBuildPart), + Math.Max(0, versionInfo.FilePrivatePart)); + } + + private static void ReplaceWithMoveFallback(string incomingPath, string targetPath, string backupPath) + { + if (File.Exists(targetPath)) + { + File.Copy(targetPath, backupPath, overwrite: true); + } + File.Move(incomingPath, targetPath, overwrite: true); + } + + internal static string ResolveResumeStartupOption(bool resumeHidden, bool resumeVisible) + => resumeHidden + ? AutoStartup.StartupHiddenOption + : resumeVisible ? AutoStartup.StartupVisibleOption : string.Empty; + + private static void TryStartRollback(string targetPath, string workingDirectory, bool resumeHidden, bool resumeVisible) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = targetPath, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + }; + string resumeStartupOption = ResolveResumeStartupOption(resumeHidden, resumeVisible); + if (!string.IsNullOrEmpty(resumeStartupOption)) + { + startInfo.ArgumentList.Add(resumeStartupOption); + } + + Process.Start(startInfo); + } + catch + { + } + } + + private static void ValidateExecutablePath(string executablePath, string parameterName) + { + if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath)) + { + throw new FileNotFoundException("The update executable does not exist.", executablePath); + } + if (!string.Equals(Path.GetExtension(executablePath), ".exe", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("The update payload must be an .exe file.", parameterName); + } + } + + private static void ValidateTargetPath(string targetPath) + { + if (string.IsNullOrWhiteSpace(targetPath)) + { + throw new ArgumentException("The target executable path is required.", nameof(targetPath)); + } + if (!string.Equals(Path.GetExtension(targetPath), ".exe", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("The update target must be an .exe file."); + } + + string fileName = Path.GetFileNameWithoutExtension(targetPath); + if (!fileName.StartsWith("Shadowsocks", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("The self-updater only replaces a Shadowsocks product executable."); + } + } + + private static void ValidateTransactionPath(string transactionDirectory, string updaterPath) + { + if (!IsCanonicalTransactionDirectory(transactionDirectory)) + { + throw new InvalidDataException("The update transaction directory is outside the Shadowsocks system-temp update root."); + } + + string expectedUpdater = Path.GetFullPath(GetTemporaryUpdaterPath(transactionDirectory)); + if (!string.Equals(expectedUpdater, Path.GetFullPath(updaterPath), StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("The updater executable is not the canonical file in its update transaction."); + } + } + + private static bool IsCanonicalBackupPath(string backupPath) + { + try + { + string executablePath = Environment.ProcessPath + ?? Process.GetCurrentProcess().MainModule?.FileName + ?? string.Empty; + if (string.IsNullOrWhiteSpace(executablePath)) + { + return false; + } + + string fullBackup = Path.GetFullPath(backupPath); + string executableDirectory = Path.GetDirectoryName(Path.GetFullPath(executablePath)) ?? string.Empty; + string backupDirectory = Path.GetDirectoryName(fullBackup) ?? string.Empty; + string expectedPrefix = "." + Path.GetFileName(executablePath) + "."; + string backupName = Path.GetFileName(fullBackup); + return string.Equals(executableDirectory, backupDirectory, StringComparison.OrdinalIgnoreCase) + && backupName.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase) + && backupName.EndsWith(".update-backup", StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + private static bool CanWriteDirectory(string directory) + { + string probe = Path.Combine(directory, $".shadowsocks-update-write-test-{Guid.NewGuid():N}.tmp"); + try + { + using (new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose)) + { + } + TryDeleteFile(probe); + return true; + } + catch (UnauthorizedAccessException) + { + return false; + } + catch (IOException) + { + return false; + } + catch (SecurityException) + { + return false; + } + } + + private static bool WaitForProcessExit(int processId, int timeoutMilliseconds) + { + if (processId <= 0) + { + return true; + } + + try + { + using Process process = Process.GetProcessById(processId); + if (process.HasExited) + { + return true; + } + return process.WaitForExit(timeoutMilliseconds); + } + catch (ArgumentException) + { + return true; + } + catch (InvalidOperationException) + { + return true; + } + } + + internal static string ComputeSha256Hex(string path) + { + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(stream)); + } + + internal static void VerifySha256(Stream stream, string expectedSha256) + { + ArgumentNullException.ThrowIfNull(stream); + + string normalized = NormalizeSha256(expectedSha256); + if (stream.CanSeek) + { + stream.Position = 0; + } + + byte[] actualHash = System.Security.Cryptography.SHA256.HashData(stream); + byte[] expectedHash = Convert.FromHexString(normalized); + if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(actualHash, expectedHash)) + { + throw new InvalidDataException("The staged update executable no longer matches its verified SHA-256 digest."); + } + } + + private static string NormalizeSha256(string value) + { + string normalized = (value ?? string.Empty).Trim(); + if (normalized.Length != 64 || !normalized.All(Uri.IsHexDigit)) + { + throw new InvalidDataException("The staged update SHA-256 digest is invalid."); + } + return normalized.ToUpperInvariant(); + } + + private static void EnsureFilesMatch(string expectedPath, string actualPath) + { + var expected = new FileInfo(expectedPath); + var actual = new FileInfo(actualPath); + if (!expected.Exists || !actual.Exists || expected.Length != actual.Length) + { + throw new IOException("The copied update executable does not match the staged payload."); + } + + using FileStream expectedStream = File.OpenRead(expectedPath); + using FileStream actualStream = File.OpenRead(actualPath); + byte[] expectedHash = System.Security.Cryptography.SHA256.HashData(expectedStream); + byte[] actualHash = System.Security.Cryptography.SHA256.HashData(actualStream); + if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(expectedHash, actualHash)) + { + throw new IOException("The copied update executable failed SHA-256 verification."); + } + } + + private static bool ContainsSwitch(IReadOnlyList arguments, string expected) + { + return arguments != null && arguments.Any(argument => + string.Equals(argument, expected, StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsInternalOption(string argument) + { + return IsOption(argument, TargetOption) + || IsOption(argument, WaitPidOption) + || IsOption(argument, TransactionOption) + || IsOption(argument, BackupOption) + || IsOption(argument, UpdaterPidOption) + || IsOption(argument, PayloadSha256Option); + } + + private static bool IsOption(string argument, string option) + { + return argument.Equals(option, StringComparison.OrdinalIgnoreCase) + || argument.StartsWith(option + "=", StringComparison.OrdinalIgnoreCase); + } + + private static string GetRequiredOption(IReadOnlyList arguments, string option) + { + string value = GetOptionalOption(arguments, option); + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidDataException($"Required update argument '{option}' is missing."); + } + return value; + } + + private static string GetOptionalOption(IReadOnlyList arguments, string option) + { + if (arguments == null) + { + return null; + } + + for (int index = 0; index < arguments.Count; index++) + { + string argument = arguments[index] ?? string.Empty; + if (argument.StartsWith(option + "=", StringComparison.OrdinalIgnoreCase)) + { + return argument.Substring(option.Length + 1).Trim('"'); + } + if (argument.Equals(option, StringComparison.OrdinalIgnoreCase) && index + 1 < arguments.Count) + { + return (arguments[index + 1] ?? string.Empty).Trim('"'); + } + } + return null; + } + + private static int ParseRequiredPid(IReadOnlyList arguments, string option) + { + string value = GetRequiredOption(arguments, option); + if (!int.TryParse(value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out int pid) + || pid <= 0) + { + throw new InvalidDataException($"Update argument '{option}' contains an invalid process ID."); + } + return pid; + } + + private static int ParseOptionalPid(IReadOnlyList arguments, string option) + { + string value = GetOptionalOption(arguments, option); + return int.TryParse(value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out int pid) + ? pid + : 0; + } + + private static string EnsureTrailingSeparator(string path) + { + string fullPath = Path.GetFullPath(path); + return fullPath.EndsWith(Path.DirectorySeparatorChar) + ? fullPath + : fullPath + Path.DirectorySeparatorChar; + } + + private static void TryDeleteFile(string path) + { + try + { + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + } + } + + private static void TryDeleteFileWithRetry(string path) + { + for (int attempt = 0; attempt < CleanupRetryCount; attempt++) + { + try + { + if (!File.Exists(path)) + { + return; + } + File.Delete(path); + return; + } + catch when (attempt + 1 < CleanupRetryCount) + { + Thread.Sleep(CleanupRetryDelayMilliseconds); + } + catch + { + return; + } + } + } + + private static void TryDeleteDirectoryWithRetry(string directory) + { + if (!IsCanonicalTransactionDirectory(directory)) + { + return; + } + + for (int attempt = 0; attempt < CleanupRetryCount; attempt++) + { + try + { + if (!Directory.Exists(directory)) + { + return; + } + Directory.Delete(directory, recursive: true); + return; + } + catch when (attempt + 1 < CleanupRetryCount) + { + Thread.Sleep(CleanupRetryDelayMilliseconds); + } + catch + { + return; + } + } + } + } +} diff --git a/shadowsocks-csharp/Controller/Service/Sip003Plugin.cs b/Shadowsocks.Windows/Controller/Service/Sip003Plugin.cs similarity index 59% rename from shadowsocks-csharp/Controller/Service/Sip003Plugin.cs rename to Shadowsocks.Windows/Controller/Service/Sip003Plugin.cs index c955c7b9c..899e4648d 100644 --- a/shadowsocks-csharp/Controller/Service/Sip003Plugin.cs +++ b/Shadowsocks.Windows/Controller/Service/Sip003Plugin.cs @@ -1,176 +1,210 @@ -using System; -using System.Collections.Specialized; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Net.Sockets; -using Shadowsocks.Model; -using Shadowsocks.Util.ProcessManagement; - -namespace Shadowsocks.Controller.Service -{ - // https://github.com/shadowsocks/shadowsocks-org/wiki/Plugin - public sealed class Sip003Plugin : IDisposable - { - public IPEndPoint LocalEndPoint { get; private set; } - public int ProcessId => _started ? _pluginProcess.Id : 0; - - private readonly object _startProcessLock = new object(); - private readonly Job _pluginJob; - private readonly Process _pluginProcess; - private bool _started; - private bool _disposed; - - public static Sip003Plugin CreateIfConfigured(Server server, bool showPluginOutput) - { - if (server == null) - { - throw new ArgumentNullException(nameof(server)); - } - - if (string.IsNullOrWhiteSpace(server.plugin)) - { - return null; - } - - return new Sip003Plugin( - server.plugin, - server.plugin_opts, - server.plugin_args, - server.server, - server.server_port, - showPluginOutput); - } - - private Sip003Plugin(string plugin, string pluginOpts, string pluginArgs, string serverAddress, int serverPort, bool showPluginOutput) - { - if (plugin == null) throw new ArgumentNullException(nameof(plugin)); - if (string.IsNullOrWhiteSpace(serverAddress)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress)); - } - if (serverPort <= 0 || serverPort > 65535) - { - throw new ArgumentOutOfRangeException("serverPort"); - } - - _pluginProcess = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = plugin, - Arguments = pluginArgs, - UseShellExecute = false, - CreateNoWindow = !showPluginOutput, - ErrorDialog = false, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = Program.WorkingDirectory ?? Environment.CurrentDirectory, - Environment = - { - ["SS_REMOTE_HOST"] = serverAddress, - ["SS_REMOTE_PORT"] = serverPort.ToString(), - ["SS_PLUGIN_OPTIONS"] = pluginOpts - } - } - }; - - _pluginJob = new Job(); - } - - public bool StartIfNeeded() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - - lock (_startProcessLock) - { - if (_started && !_pluginProcess.HasExited) - { - return false; - } - - var localPort = GetNextFreeTcpPort(); - LocalEndPoint = new IPEndPoint(IPAddress.Loopback, localPort); - - _pluginProcess.StartInfo.Environment["SS_LOCAL_HOST"] = LocalEndPoint.Address.ToString(); - _pluginProcess.StartInfo.Environment["SS_LOCAL_PORT"] = LocalEndPoint.Port.ToString(); - _pluginProcess.StartInfo.Arguments = ExpandEnvironmentVariables(_pluginProcess.StartInfo.Arguments, _pluginProcess.StartInfo.EnvironmentVariables); - try - { - _pluginProcess.Start(); - } - catch (System.ComponentModel.Win32Exception ex) - { - // do not use File.Exists(...), it can not handle the scenarios when the plugin file is in system environment path. - // https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values - //if ((uint)ex.ErrorCode == 0x80004005) - // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d - if (ex.NativeErrorCode == 0x00000002) - { - throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), _pluginProcess.StartInfo.FileName, ex); - } - throw new ApplicationException(I18N.GetString("Plugin Program"), ex); - } - _pluginJob.AddProcess(_pluginProcess.Handle); - _started = true; - } - - return true; - } - - public string ExpandEnvironmentVariables(string name, StringDictionary environmentVariables = null) - { - // Expand the environment variables from the new process itself - if (environmentVariables != null) - { - foreach(string key in environmentVariables.Keys) - { - name = name.Replace($"%{key}%", environmentVariables[key]); - } - } - // Also expand the environment variables from current main process (system) - name = Environment.ExpandEnvironmentVariables(name); - return name; - } - - static int GetNextFreeTcpPort() - { - var l = new TcpListener(IPAddress.Loopback, 0); - l.Start(); - int port = ((IPEndPoint)l.LocalEndpoint).Port; - l.Stop(); - return port; - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - try - { - if (!_pluginProcess.HasExited) - { - _pluginProcess.Kill(); - _pluginProcess.WaitForExit(); - } - } - catch (Exception) { } - finally - { - try - { - _pluginProcess.Dispose(); - _pluginJob.Dispose(); - } - catch (Exception) { } - - _disposed = true; - } - } - } -} \ No newline at end of file +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using NLog; +using Shadowsocks.Model; +using Shadowsocks.Core.Storage; +using Shadowsocks.Util.ProcessManagement; + +namespace Shadowsocks.Controller.Service +{ + // https://github.com/shadowsocks/shadowsocks-org/wiki/Plugin + public sealed class Sip003Plugin : IDisposable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public IPEndPoint LocalEndPoint { get; private set; } + public int ProcessId => _started ? _pluginProcess.Id : 0; + + private readonly object _startProcessLock = new object(); + private readonly Job _pluginJob; + private readonly Process _pluginProcess; + private readonly Func _showPluginOutputProvider; + private bool _started; + private bool _disposed; + + public static Sip003Plugin CreateIfConfigured(Server server, bool showPluginOutput) + => CreateIfConfigured(server, () => showPluginOutput); + + public static Sip003Plugin CreateIfConfigured(Server server, Func showPluginOutputProvider) + { + ArgumentNullException.ThrowIfNull(server); + + if (string.IsNullOrWhiteSpace(server.plugin)) + { + return null; + } + + return new Sip003Plugin( + server.plugin, + server.PluginOptions, + server.PluginArguments, + server.server, + server.ServerPort, + showPluginOutputProvider); + } + + private Sip003Plugin(string plugin, string pluginOpts, string pluginArgs, string serverAddress, int serverPort, Func showPluginOutputProvider) + { + ArgumentNullException.ThrowIfNull(plugin); + _showPluginOutputProvider = showPluginOutputProvider ?? (() => false); + if (string.IsNullOrWhiteSpace(serverAddress)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress)); + } + if (serverPort <= 0 || serverPort > 65535) + { + throw new ArgumentOutOfRangeException(nameof(serverPort)); + } + + string resolvedPlugin = PluginManager.ResolveExecutable(plugin); + if (string.IsNullOrWhiteSpace(resolvedPlugin)) + { + throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), plugin); + } + string pluginWorkingDirectory = AppStoragePaths.EnsureTempDirectory( + Path.Combine(AppStoragePaths.TempWorkingRoot, "Plugins")); + + _pluginProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = resolvedPlugin, + Arguments = pluginArgs, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + ErrorDialog = false, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = pluginWorkingDirectory, + Environment = + { + ["SS_REMOTE_HOST"] = serverAddress, + ["SS_REMOTE_PORT"] = serverPort.ToString(CultureInfo.InvariantCulture), + ["SS_PLUGIN_OPTIONS"] = pluginOpts + } + } + }; + _pluginProcess.OutputDataReceived += OnPluginOutputDataReceived; + _pluginProcess.ErrorDataReceived += OnPluginErrorDataReceived; + + _pluginJob = new Job(); + } + + public bool StartIfNeeded() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + lock (_startProcessLock) + { + if (_started && !_pluginProcess.HasExited) + { + return false; + } + + var localPort = GetNextFreeTcpPort(); + LocalEndPoint = new IPEndPoint(IPAddress.Loopback, localPort); + + _pluginProcess.StartInfo.Environment["SS_LOCAL_HOST"] = LocalEndPoint.Address.ToString(); + _pluginProcess.StartInfo.Environment["SS_LOCAL_PORT"] = LocalEndPoint.Port.ToString(CultureInfo.InvariantCulture); + _pluginProcess.StartInfo.Arguments = ExpandEnvironmentVariables(_pluginProcess.StartInfo.Arguments, _pluginProcess.StartInfo.EnvironmentVariables); + try + { + _pluginProcess.Start(); + } + catch (System.ComponentModel.Win32Exception ex) + { + if (ex.NativeErrorCode == 0x00000002) + { + throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), _pluginProcess.StartInfo.FileName, ex); + } + throw new InvalidOperationException(I18N.GetString("Plugin Program"), ex); + } + _pluginProcess.BeginOutputReadLine(); + _pluginProcess.BeginErrorReadLine(); + _pluginJob.AddProcess(_pluginProcess.Handle); + _started = true; + } + + return true; + } + + private void OnPluginOutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrWhiteSpace(e.Data) && IsPluginOutputEnabled()) + Logger.Info("SIP003 | STDOUT | {0}", e.Data); + } + + private void OnPluginErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrWhiteSpace(e.Data) && IsPluginOutputEnabled()) + Logger.Warn("SIP003 | STDERR | {0}", e.Data); + } + + private bool IsPluginOutputEnabled() + { + try { return _showPluginOutputProvider(); } + catch { return false; } + } + + public static string ExpandEnvironmentVariables(string name, StringDictionary environmentVariables = null) + { + // Expand the environment variables from the new process itself + if (environmentVariables != null) + { + foreach (string key in environmentVariables.Keys) + { + name = name.Replace($"%{key}%", environmentVariables[key]); + } + } + // Also expand the environment variables from current main process (system) + name = Environment.ExpandEnvironmentVariables(name); + return name; + } + + static int GetNextFreeTcpPort() + { + var l = new TcpListener(IPAddress.Loopback, 0); + l.Start(); + int port = ((IPEndPoint)l.LocalEndpoint).Port; + l.Stop(); + return port; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + try + { + if (!_pluginProcess.HasExited) + { + _pluginProcess.Kill(); + _pluginProcess.WaitForExit(1500); + } + } + catch (Exception) { } + finally + { + try + { + _pluginProcess.OutputDataReceived -= OnPluginOutputDataReceived; + _pluginProcess.ErrorDataReceived -= OnPluginErrorDataReceived; + _pluginProcess.Dispose(); + _pluginJob.Dispose(); + } + catch (Exception) { } + + _disposed = true; + } + } + } +} diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/Shadowsocks.Windows/Controller/Service/TCPRelay.cs similarity index 89% rename from shadowsocks-csharp/Controller/Service/TCPRelay.cs rename to Shadowsocks.Windows/Controller/Service/TCPRelay.cs index c7141a6b3..e8c487d56 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/Shadowsocks.Windows/Controller/Service/TCPRelay.cs @@ -1,1064 +1,1071 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Timers; - -using NLog; - -using Shadowsocks.Controller.Strategy; -using Shadowsocks.Encryption; -using Shadowsocks.Encryption.AEAD; -using Shadowsocks.Encryption.Exception; -using Shadowsocks.Model; -using Shadowsocks.Proxy; -using Shadowsocks.Util.Sockets; - -using static Shadowsocks.Encryption.EncryptorBase; - -namespace Shadowsocks.Controller -{ - internal class TCPRelay : Listener.Service - { - public event EventHandler OnConnected; - public event EventHandler OnInbound; - public event EventHandler OnOutbound; - public event EventHandler OnFailed; - - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - private readonly ShadowsocksController _controller; - private DateTime _lastSweepTime; - private readonly Configuration _config; - - public ISet Handlers { get; set; } - - public TCPRelay(ShadowsocksController controller, Configuration conf) - { - _controller = controller; - _config = conf; - Handlers = new HashSet(); - _lastSweepTime = DateTime.Now; - } - - public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) - { - if (socket.ProtocolType != ProtocolType.Tcp - || (length < 2 || firstPacket[0] != 5)) - { - return false; - } - - socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - TCPHandler handler = new TCPHandler(_controller, _config, socket); - - handler.OnConnected += OnConnected; - handler.OnInbound += OnInbound; - handler.OnOutbound += OnOutbound; - handler.OnFailed += OnFailed; - handler.OnClosed += (h, arg) => - { - lock (Handlers) - { - Handlers.Remove(handler); - } - }; - - IList handlersToClose = new List(); - lock (Handlers) - { - Handlers.Add(handler); - DateTime now = DateTime.Now; - if (now - _lastSweepTime > TimeSpan.FromSeconds(1)) - { - _lastSweepTime = now; - foreach (TCPHandler handler1 in Handlers) - { - if (now - handler1.lastActivity > TimeSpan.FromSeconds(900)) - { - handlersToClose.Add(handler1); - } - } - } - } - foreach (TCPHandler handler1 in handlersToClose) - { - logger.Debug("Closing timed out TCP connection."); - handler1.Close(); - } - - /* - * Start after we put it into Handlers set. Otherwise if it failed in handler.Start() - * then it will call handler.Close() before we add it into the set. - * Then the handler will never release until the next Handle call. Sometimes it will - * cause odd problems (especially during memory profiling). - */ - handler.Start(firstPacket, length); - - return true; - } - - public override void Stop() - { - List handlersToClose = new List(); - lock (Handlers) - { - handlersToClose.AddRange(Handlers); - } - handlersToClose.ForEach(h => h.Close()); - } - } - - public class SSRelayEventArgs : EventArgs - { - public readonly Server server; - - public SSRelayEventArgs(Server server) - { - this.server = server; - } - } - - public class SSTransmitEventArgs : SSRelayEventArgs - { - public readonly long length; - public SSTransmitEventArgs(Server server, long length) : base(server) - { - this.length = length; - } - } - - public class SSTCPConnectedEventArgs : SSRelayEventArgs - { - public readonly TimeSpan latency; - - public SSTCPConnectedEventArgs(Server server, TimeSpan latency) : base(server) - { - this.latency = latency; - } - } - - internal class TCPHandler - { - public event EventHandler OnConnected; - public event EventHandler OnInbound; - public event EventHandler OnOutbound; - public event EventHandler OnClosed; - public event EventHandler OnFailed; - - private class AsyncSession - { - public IProxy Remote { get; } - - public AsyncSession(IProxy remote) - { - Remote = remote; - } - } - - private class AsyncSession : AsyncSession - { - public T State { get; set; } - - public AsyncSession(IProxy remote, T state) : base(remote) - { - State = state; - } - - public AsyncSession(AsyncSession session, T state) : base(session.Remote) - { - State = state; - } - } - - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - - private readonly int _serverTimeout; - private readonly int _proxyTimeout; - - // each recv size. - public const int RecvSize = 2048; - - // overhead of one chunk, reserved for AEAD ciphers - public const int ChunkOverheadSize = 16 * 2 /* two tags */ + AEADEncryptor.CHUNK_LEN_BYTES; - - // max chunk size - public const uint MaxChunkSize = AEADEncryptor.CHUNK_LEN_MASK + AEADEncryptor.CHUNK_LEN_BYTES + 16 * 2; - - // In general, the ciphertext length, we should take overhead into account - public const int BufferSize = RecvSize + (int)MaxChunkSize + 32 /* max salt len */; - - public DateTime lastActivity; - - private readonly ShadowsocksController _controller; - private readonly ForwardProxyConfig _config; - private readonly Socket _connection; - - private IEncryptor _encryptor; - private Server _server; - - private AsyncSession _currentRemoteSession; - - private bool _proxyConnected; - private bool _destConnected; - - private byte _command; - private byte[] _firstPacket; - private int _firstPacketLength; - - private const int CMD_CONNECT = 0x01; - private const int CMD_BIND = 0x02; - private const int CMD_UDP_ASSOC = 0x03; - - private int _addrBufLength = -1; - - private int _totalRead = 0; - private int _totalWrite = 0; - - // remote -> local proxy (ciphertext, before decrypt) - private readonly byte[] _remoteRecvBuffer = new byte[BufferSize]; - - // client -> local proxy (plaintext, before encrypt) - private readonly byte[] _connetionRecvBuffer = new byte[BufferSize]; - - // local proxy -> remote (plaintext, after decrypt) - private readonly byte[] _remoteSendBuffer = new byte[BufferSize]; - - // local proxy -> client (ciphertext, before decrypt) - private readonly byte[] _connetionSendBuffer = new byte[BufferSize]; - - private bool _connectionShutdown = false; - private bool _remoteShutdown = false; - private bool _closed = false; - - // instance-based lock without static - private readonly object _encryptionLock = new object(); - - private readonly object _decryptionLock = new object(); - private readonly object _closeConnLock = new object(); - - private DateTime _startConnectTime; - private DateTime _startReceivingTime; - private DateTime _startSendingTime; - - private EndPoint _destEndPoint = null; - - // TODO: decouple controller - public TCPHandler(ShadowsocksController controller, Configuration config, Socket socket) - { - _controller = controller; - _config = config.proxy; - _connection = socket; - _proxyTimeout = config.proxy.proxyTimeout * 1000; - _serverTimeout = config.GetCurrentServer().timeout * 1000; - - lastActivity = DateTime.Now; - } - - public void CreateRemote() - { - Server server = _controller.GetAServer(IStrategyCallerType.TCP, (IPEndPoint)_connection.RemoteEndPoint, - _destEndPoint); - if (server == null || server.server == "") - { - throw new ArgumentException("No server configured"); - } - - _encryptor = EncryptorFactory.GetEncryptor(server.method, server.password); - - _server = server; - - /* prepare address buffer length for AEAD */ - Logger.Trace($"_addrBufLength={_addrBufLength}"); - _encryptor.AddrBufLength = _addrBufLength; - } - - public void Start(byte[] firstPacket, int length) - { - _firstPacket = firstPacket; - _firstPacketLength = length; - HandshakeReceive(); - } - - private void CheckClose() - { - if (_connectionShutdown && _remoteShutdown) - { - Close(); - } - } - - private void ErrorClose(Exception e) - { - Logger.LogUsefulException(e); - Close(); - } - - public void Close() - { - lock (_closeConnLock) - { - if (_closed) - { - return; - } - - _closed = true; - } - - OnClosed?.Invoke(this, new SSRelayEventArgs(_server)); - - try - { - _connection.Shutdown(SocketShutdown.Both); - _connection.Close(); - } - catch (Exception e) - { - Logger.LogUsefulException(e); - } - - if (_currentRemoteSession != null) - { - try - { - IProxy remote = _currentRemoteSession.Remote; - remote.Shutdown(SocketShutdown.Both); - remote.Close(); - } - catch (Exception e) - { - Logger.LogUsefulException(e); - } - } - - lock (_encryptionLock) - { - lock (_decryptionLock) - { - _encryptor?.Dispose(); - } - } - } - - private void HandshakeReceive() - { - if (_closed) - { - return; - } - - try - { - int bytesRead = _firstPacketLength; - if (bytesRead > 1) - { - byte[] response = { 5, 0 }; - if (_firstPacket[0] != 5) - { - // reject socks 4 - response = new byte[] { 0, 91 }; - Logger.Error("socks 5 protocol error"); - } - _connection.BeginSend(response, 0, response.Length, SocketFlags.None, - HandshakeSendCallback, null); - } - else - { - Close(); - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void HandshakeSendCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - _connection.EndSend(ar); - - // +-----+-----+-------+------+----------+----------+ - // | VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | - // +-----+-----+-------+------+----------+----------+ - // | 1 | 1 | X'00' | 1 | Variable | 2 | - // +-----+-----+-------+------+----------+----------+ - // Skip first 3 bytes, and read 2 more bytes to analysis the address. - // 2 more bytes is designed if address is domain then we don't need to read once more to get the addr length. - // validate is unnecessary, we did it in first packet, but we can do it in future version - _connection.BeginReceive(_connetionRecvBuffer, 0, 3 + ADDR_ATYP_LEN + 1, SocketFlags.None, - AddressReceiveCallback, null); - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void AddressReceiveCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - int bytesRead = _connection.EndReceive(ar); - if (bytesRead >= 5) - { - _command = _connetionRecvBuffer[1]; - switch (_command) - { - case CMD_CONNECT: - - // +----+-----+-------+------+----------+----------+ - // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | - // +----+-----+-------+------+----------+----------+ - // | 1 | 1 | X'00' | 1 | Variable | 2 | - // +----+-----+-------+------+----------+----------+ - byte[] response = { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }; - _connection.BeginSend(response, 0, response.Length, SocketFlags.None, - ConnectResponseCallback, null); - break; - case CMD_UDP_ASSOC: - ReadAddress(HandleUDPAssociate); - break; - case CMD_BIND: // not implemented - default: - Logger.Debug("Unsupported CMD=" + _command); - Close(); - break; - } - } - else - { - Logger.Debug( - "failed to recv data in Shadowsocks.Controller.TCPHandler.handshakeReceive2Callback()"); - Close(); - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void ConnectResponseCallback(IAsyncResult ar) - { - try - { - _connection.EndSend(ar); - - ReadAddress(StartConnect); - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void ReadAddress(Action onSuccess) - { - int atyp = _connetionRecvBuffer[3]; - - switch (atyp) - { - case ATYP_IPv4: // IPv4 address, 4 bytes - ReadAddress(4 + ADDR_PORT_LEN - 1, onSuccess); - break; - case ATYP_DOMAIN: // domain name, length + str - int len = _connetionRecvBuffer[4]; - ReadAddress(len + ADDR_PORT_LEN, onSuccess); - break; - case ATYP_IPv6: // IPv6 address, 16 bytes - ReadAddress(16 + ADDR_PORT_LEN - 1, onSuccess); - break; - default: - Logger.Debug("Unsupported ATYP=" + atyp); - Close(); - break; - } - } - - private void ReadAddress(int bytesRemain, Action onSuccess) - { - // drop [ VER | CMD | RSV ] - Array.Copy(_connetionRecvBuffer, 3, _connetionRecvBuffer, 0, ADDR_ATYP_LEN + 1); - - // Read the remain address bytes - _connection.BeginReceive(_connetionRecvBuffer, 2, RecvSize - 2, SocketFlags.None, OnAddressFullyRead, - new object[] { bytesRemain, onSuccess }); - } - - private void OnAddressFullyRead(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - int bytesRead = _connection.EndReceive(ar); - - object[] states = (object[])ar.AsyncState; - - int bytesRemain = (int)states[0]; - Action onSuccess = (Action)states[1]; - - if (bytesRead >= bytesRemain) - { - _firstPacketLength = bytesRead + 2; - - int atyp = _connetionRecvBuffer[0]; - - string dstAddr = "Unknown"; - int dstPort = -1; - switch (atyp) - { - case ATYP_IPv4: // IPv4 address, 4 bytes - dstAddr = new IPAddress(_connetionRecvBuffer.Skip(1).Take(4).ToArray()).ToString(); - dstPort = (_connetionRecvBuffer[5] << 8) + _connetionRecvBuffer[6]; - - _addrBufLength = ADDR_ATYP_LEN + 4 + ADDR_PORT_LEN; - break; - case ATYP_DOMAIN: // domain name, length + str - int len = _connetionRecvBuffer[1]; - dstAddr = System.Text.Encoding.UTF8.GetString(_connetionRecvBuffer, 2, len); - dstPort = (_connetionRecvBuffer[len + 2] << 8) + _connetionRecvBuffer[len + 3]; - - _addrBufLength = ADDR_ATYP_LEN + 1 + len + ADDR_PORT_LEN; - break; - case ATYP_IPv6: // IPv6 address, 16 bytes - dstAddr = $"[{new IPAddress(_connetionRecvBuffer.Skip(1).Take(16).ToArray())}]"; - dstPort = (_connetionRecvBuffer[17] << 8) + _connetionRecvBuffer[18]; - - _addrBufLength = ADDR_ATYP_LEN + 16 + ADDR_PORT_LEN; - break; - } - - Logger.Debug($"connect to {dstAddr}:{dstPort}"); - - _destEndPoint = SocketUtil.GetEndPoint(dstAddr, dstPort); - - onSuccess.Invoke(); /* StartConnect() */ - } - else - { - Logger.Debug("failed to recv data in Shadowsocks.Controller.TCPHandler.OnAddressFullyRead()"); - Close(); - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void HandleUDPAssociate() - { - IPEndPoint endPoint = (IPEndPoint)_connection.LocalEndPoint; - byte[] address = endPoint.Address.GetAddressBytes(); - int port = endPoint.Port; - byte[] response = new byte[4 + address.Length + ADDR_PORT_LEN]; - response[0] = 5; - switch (endPoint.AddressFamily) - { - case AddressFamily.InterNetwork: - response[3] = ATYP_IPv4; - break; - case AddressFamily.InterNetworkV6: - response[3] = ATYP_IPv6; - break; - } - address.CopyTo(response, 4); - response[response.Length - 1] = (byte)(port & 0xFF); - response[response.Length - 2] = (byte)((port >> 8) & 0xFF); - _connection.BeginSend(response, 0, response.Length, SocketFlags.None, ReadAll, true); - } - - private void ReadAll(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - if (ar.AsyncState != null) - { - _connection.EndSend(ar); - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, - ReadAll, null); - } - else - { - int bytesRead = _connection.EndReceive(ar); - if (bytesRead > 0) - { - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, - ReadAll, null); - } - else - { - Close(); - } - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - // inner class - private class ProxyTimer : Timer - { - public AsyncSession Session; - - public EndPoint DestEndPoint; - public Server Server; - - public ProxyTimer(int p) : base(p) - { - } - } - - private class ServerTimer : Timer - { - public AsyncSession Session; - - public Server Server; - - public ServerTimer(int p) : base(p) - { - } - } - - private void StartConnect() - { - try - { - CreateRemote(); - - // Setting up proxy - IProxy remote; - EndPoint proxyEP = null; - EndPoint serverEP = SocketUtil.GetEndPoint(_server.server, _server.server_port); - EndPoint pluginEP = _controller.GetPluginLocalEndPointIfConfigured(_server); - - if (pluginEP != null) - { - serverEP = pluginEP; - remote = new DirectConnect(); - } - else if (_config.useProxy) - { - switch (_config.proxyType) - { - case ForwardProxyConfig.PROXY_SOCKS5: - remote = new Socks5Proxy(); - break; - case ForwardProxyConfig.PROXY_HTTP: - remote = new HttpProxy(); - break; - default: - throw new NotSupportedException("Unknown forward proxy."); - } - proxyEP = SocketUtil.GetEndPoint(_config.proxyServer, _config.proxyPort); - } - else - { - remote = new DirectConnect(); - } - - AsyncSession session = new AsyncSession(remote); - lock (_closeConnLock) - { - if (_closed) - { - remote.Close(); - return; - } - - _currentRemoteSession = session; - } - - ProxyTimer proxyTimer = new ProxyTimer(_proxyTimeout) { AutoReset = false }; - proxyTimer.Elapsed += ProxyConnectTimer_Elapsed; - proxyTimer.Enabled = true; - - proxyTimer.Session = session; - proxyTimer.DestEndPoint = serverEP; - proxyTimer.Server = _server; - - _proxyConnected = false; - - // Connect to the proxy server. - remote.BeginConnectProxy(proxyEP, ProxyConnectCallback, - new AsyncSession(remote, proxyTimer)); - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void ProxyConnectTimer_Elapsed(object sender, ElapsedEventArgs e) - { - ProxyTimer timer = (ProxyTimer)sender; - timer.Elapsed -= ProxyConnectTimer_Elapsed; - timer.Enabled = false; - timer.Dispose(); - - - if (_proxyConnected || _destConnected || _closed) - { - return; - } - IProxy proxy = timer.Session.Remote; - - Logger.Info($"Proxy {proxy.ProxyEndPoint} timed out"); - proxy.Close(); - Close(); - } - - private void ProxyConnectCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - AsyncSession session = (AsyncSession)ar.AsyncState; - ProxyTimer timer = session.State; - EndPoint destEndPoint = timer.DestEndPoint; - Server server = timer.Server; - timer.Elapsed -= ProxyConnectTimer_Elapsed; - timer.Enabled = false; - timer.Dispose(); - - IProxy remote = session.Remote; - - // Complete the connection. - remote.EndConnectProxy(ar); - - _proxyConnected = true; - - if (!(remote is DirectConnect)) - { - Logger.Debug($"Socket connected to proxy {remote.ProxyEndPoint}"); - } - - _startConnectTime = DateTime.Now; - ServerTimer connectTimer = new ServerTimer(_serverTimeout) { AutoReset = false }; - connectTimer.Elapsed += DestConnectTimer_Elapsed; - connectTimer.Enabled = true; - connectTimer.Session = session; - connectTimer.Server = server; - - _destConnected = false; - - NetworkCredential auth = null; - if (_config.useAuth) - { - auth = new NetworkCredential(_config.authUser, _config.authPwd); - } - - // Connect to the remote endpoint. - remote.BeginConnectDest(destEndPoint, ConnectCallback, - new AsyncSession(session, connectTimer), auth); - } - catch (ArgumentException) - { - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void DestConnectTimer_Elapsed(object sender, ElapsedEventArgs e) - { - ServerTimer timer = (ServerTimer)sender; - timer.Elapsed -= DestConnectTimer_Elapsed; - timer.Enabled = false; - timer.Dispose(); - - if (_destConnected || _closed) - { - return; - } - - AsyncSession session = timer.Session; - Server server = timer.Server; - OnFailed?.Invoke(this, new SSRelayEventArgs(_server)); - Logger.Info($"{server.ToString()} timed out"); - session.Remote.Close(); - Close(); - } - - private void ConnectCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - AsyncSession session = (AsyncSession)ar.AsyncState; - ServerTimer timer = session.State; - _server = timer.Server; - timer.Elapsed -= DestConnectTimer_Elapsed; - timer.Enabled = false; - timer.Dispose(); - - IProxy remote = session.Remote; - // Complete the connection. - remote.EndConnectDest(ar); - - _destConnected = true; - - Logger.Debug($"Socket connected to ss server: {_server.ToString()}"); - - TimeSpan latency = DateTime.Now - _startConnectTime; - - OnConnected?.Invoke(this, new SSTCPConnectedEventArgs(_server, latency)); - - StartPipe(session); - } - catch (ArgumentException) - { - } - catch (Exception e) - { - if (_server != null) - { - OnFailed?.Invoke(this, new SSRelayEventArgs(_server)); - } - ErrorClose(e); - } - } - - private void TryReadAvailableData() - { - int available = Math.Min(_connection.Available, RecvSize - _firstPacketLength); - if (available > 0) - { - int size = _connection.Receive(_connetionRecvBuffer, _firstPacketLength, available, - SocketFlags.None); - - _firstPacketLength += size; - } - } - - private void StartPipe(AsyncSession session) - { - if (_closed) - { - return; - } - - try - { - _startReceivingTime = DateTime.Now; - session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, - PipeRemoteReceiveCallback, session); - - TryReadAvailableData(); - Logger.Trace($"_firstPacketLength = {_firstPacketLength}"); - SendToServer(_firstPacketLength, session); - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void PipeRemoteReceiveCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - AsyncSession session = (AsyncSession)ar.AsyncState; - int bytesRead = session.Remote.EndReceive(ar); - _totalRead += bytesRead; - - OnInbound?.Invoke(this, new SSTransmitEventArgs(_server, bytesRead)); - if (bytesRead > 0) - { - lastActivity = DateTime.Now; - int bytesToSend = -1; - lock (_decryptionLock) - { - try - { - _encryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend); - } - catch (CryptoErrorException) - { - Logger.Error("decryption error"); - Close(); - return; - } - } - if (bytesToSend == 0) - { - // need more to decrypt - Logger.Trace("Need more to decrypt"); - session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, - PipeRemoteReceiveCallback, session); - return; - } - Logger.Trace($"start sending {bytesToSend}"); - _connection.BeginSend(_remoteSendBuffer, 0, bytesToSend, SocketFlags.None, - PipeConnectionSendCallback, new object[] { session, bytesToSend }); - } - else - { - _connection.Shutdown(SocketShutdown.Send); - _connectionShutdown = true; - CheckClose(); - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void PipeConnectionReceiveCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - int bytesRead = _connection.EndReceive(ar); - - AsyncSession session = (AsyncSession)ar.AsyncState; - IProxy remote = session.Remote; - - if (bytesRead > 0) - { - SendToServer(bytesRead, session); - } - else - { - remote.Shutdown(SocketShutdown.Send); - _remoteShutdown = true; - CheckClose(); - } - } - catch (Exception e) - { - ErrorClose(e); - } - } - - private void SendToServer(int length, AsyncSession session) - { - _totalWrite += length; - int bytesToSend; - lock (_encryptionLock) - { - try - { - _encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend); - } - catch (CryptoErrorException) - { - Logger.Debug("encryption error"); - Close(); - return; - } - } - - OnOutbound?.Invoke(this, new SSTransmitEventArgs(_server, bytesToSend)); - _startSendingTime = DateTime.Now; - session.Remote.BeginSend(_connetionSendBuffer, 0, bytesToSend, SocketFlags.None, - PipeRemoteSendCallback, new object[] { session, bytesToSend }); - } - - private void PipeRemoteSendCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - - try - { - object[] container = (object[])ar.AsyncState; - AsyncSession session = (AsyncSession)container[0]; - int bytesShouldSend = (int)container[1]; - int bytesSent = session.Remote.EndSend(ar); - - if (bytesSent > 0) - { - lastActivity = DateTime.Now; - } - - int bytesRemaining = bytesShouldSend - bytesSent; - if (bytesRemaining > 0) - { - Logger.Info("reconstruct _connetionSendBuffer to re-send"); - Buffer.BlockCopy(_connetionSendBuffer, bytesSent, _connetionSendBuffer, 0, bytesRemaining); - session.Remote.BeginSend(_connetionSendBuffer, 0, bytesRemaining, SocketFlags.None, - PipeRemoteSendCallback, new object[] { session, bytesRemaining }); - return; - } - _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, - PipeConnectionReceiveCallback, session); - } - catch (Exception e) - { - ErrorClose(e); - } - } - - // In general, we assume there is no delay between local proxy and client, add this for sanity - private void PipeConnectionSendCallback(IAsyncResult ar) - { - try - { - object[] container = (object[])ar.AsyncState; - AsyncSession session = (AsyncSession)container[0]; - int bytesShouldSend = (int)container[1]; - int bytesSent = _connection.EndSend(ar); - int bytesRemaining = bytesShouldSend - bytesSent; - if (bytesRemaining > 0) - { - Logger.Info("reconstruct _remoteSendBuffer to re-send"); - Buffer.BlockCopy(_remoteSendBuffer, bytesSent, _remoteSendBuffer, 0, bytesRemaining); - _connection.BeginSend(_remoteSendBuffer, 0, bytesRemaining, SocketFlags.None, - PipeConnectionSendCallback, new object[] { session, bytesRemaining }); - return; - } - session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, - PipeRemoteReceiveCallback, session); - } - catch (Exception e) - { - ErrorClose(e); - } - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Timers; + +using NLog; + +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Strategy; +using Shadowsocks.Encryption; +using Shadowsocks.Encryption.AEAD; +using Shadowsocks.Encryption.Exception; +using Shadowsocks.Model; +using Shadowsocks.Proxy; +using Shadowsocks.Util.Sockets; + +using static Shadowsocks.Encryption.EncryptorBase; + +namespace Shadowsocks.Controller +{ + internal class TCPRelay : Listener.Service + { + public event EventHandler OnConnected; + public event EventHandler OnInbound; + public event EventHandler OnOutbound; + public event EventHandler OnFailed; + + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private readonly ShadowsocksController _controller; + private DateTime _lastSweepTime; + private readonly Configuration _config; + + public ISet Handlers { get; set; } + + public TCPRelay(ShadowsocksController controller, Configuration conf) + { + _controller = controller; + _config = conf; + Handlers = new HashSet(); + _lastSweepTime = DateTime.Now; + } + + public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) + { + if (socket.ProtocolType != ProtocolType.Tcp + || (length < 2 || firstPacket[0] != 5)) + { + return false; + } + + socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + TCPHandler handler = new TCPHandler(_controller, _config, socket); + + handler.OnConnected += OnConnected; + handler.OnInbound += OnInbound; + handler.OnOutbound += OnOutbound; + handler.OnFailed += OnFailed; + handler.OnClosed += (h, arg) => + { + lock (Handlers) + { + Handlers.Remove(handler); + } + }; + + List handlersToClose = new(); + lock (Handlers) + { + Handlers.Add(handler); + DateTime now = DateTime.Now; + if (now - _lastSweepTime > TimeSpan.FromSeconds(1)) + { + _lastSweepTime = now; + foreach (TCPHandler handler1 in Handlers) + { + if (now - handler1.lastActivity > TimeSpan.FromSeconds(900)) + { + handlersToClose.Add(handler1); + } + } + } + } + foreach (TCPHandler handler1 in handlersToClose) + { + logger.Debug("Closing timed out TCP connection."); + handler1.Close(); + } + + /* + * Start after we put it into Handlers set. Otherwise if it failed in handler.Start() + * then it will call handler.Close() before we add it into the set. + * Then the handler will never release until the next Handle call. Sometimes it will + * cause odd problems (especially during memory profiling). + */ + handler.Start(firstPacket, length); + + return true; + } + + public override void Shutdown() + { + List handlersToClose = new List(); + lock (Handlers) + { + handlersToClose.AddRange(Handlers); + } + handlersToClose.ForEach(h => h.Close()); + } + } + + public class SSRelayEventArgs : EventArgs + { + public SSRelayEventArgs(Server server) + { + Server = server; + } + + public Server Server { get; } + } + + public class SSTransmitEventArgs : SSRelayEventArgs + { + public SSTransmitEventArgs(Server server, long length) : base(server) + { + Length = length; + } + + public long Length { get; } + } + + public class SSTCPConnectedEventArgs : SSRelayEventArgs + { + public SSTCPConnectedEventArgs(Server server, TimeSpan latency) : base(server) + { + Latency = latency; + } + + public TimeSpan Latency { get; } + } + + internal sealed class TCPHandler : IDisposable + { + public event EventHandler OnConnected; + public event EventHandler OnInbound; + public event EventHandler OnOutbound; + public event EventHandler OnClosed; + public event EventHandler OnFailed; + + private class AsyncSession + { + public IProxy Remote { get; } + + public AsyncSession(IProxy remote) + { + Remote = remote; + } + } + + private class AsyncSession : AsyncSession + { + public T State { get; set; } + + public AsyncSession(IProxy remote, T state) : base(remote) + { + State = state; + } + + public AsyncSession(AsyncSession session, T state) : base(session.Remote) + { + State = state; + } + } + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private int _serverTimeout; + private readonly int _proxyTimeout; + + // each recv size. + public const int RecvSize = TransportBufferSizes.ReceiveSize; + + // overhead of one chunk, reserved for AEAD ciphers + public const int ChunkOverheadSize = TransportBufferSizes.ChunkOverheadSize; + + // max chunk size + public const uint MaxChunkSize = TransportBufferSizes.MaxChunkSize; + + // In general, the ciphertext length, we should take overhead into account + public const int BufferSize = TransportBufferSizes.BufferSize; + + public DateTime lastActivity; + + private readonly ShadowsocksController _controller; + private readonly ForwardProxyConfig _config; + private readonly Socket _connection; + + private IEncryptor _encryptor; + private Server _server; + + private AsyncSession _currentRemoteSession; + + private bool _proxyConnected; + private bool _destConnected; + + private byte _command; + private byte[] _firstPacket; + private int _firstPacketLength; + + private const int CMD_CONNECT = 0x01; + private const int CMD_BIND = 0x02; + private const int CMD_UDP_ASSOC = 0x03; + + private int _addrBufLength = -1; + + private int _totalRead; + private int _totalWrite; + + // remote -> local proxy (ciphertext, before decrypt) + private readonly byte[] _remoteRecvBuffer = new byte[BufferSize]; + + // client -> local proxy (plaintext, before encrypt) + private readonly byte[] _connetionRecvBuffer = new byte[BufferSize]; + + // local proxy -> remote (plaintext, after decrypt) + private readonly byte[] _remoteSendBuffer = new byte[BufferSize]; + + // local proxy -> client (ciphertext, before decrypt) + private readonly byte[] _connetionSendBuffer = new byte[BufferSize]; + + private bool _connectionShutdown; + private bool _remoteShutdown; + private bool _closed; + + // instance-based lock without static + private readonly object _encryptionLock = new object(); + + private readonly object _decryptionLock = new object(); + private readonly object _closeConnLock = new object(); + + private DateTime _startConnectTime; + private DateTime _startReceivingTime; + private DateTime _startSendingTime; + + private EndPoint _destEndPoint; + + public TCPHandler(ShadowsocksController controller, Configuration config, Socket socket) + { + _controller = controller; + _config = config.proxy; + _connection = socket; + _proxyTimeout = config.proxy.proxyTimeout * 1000; + + lastActivity = DateTime.Now; + } + + public void CreateRemote() + { + Server server = _controller.GetAServer(IStrategyCallerType.TCP, (IPEndPoint)_connection.RemoteEndPoint, + _destEndPoint); + if (server?.IsConfigured != true) + { + throw new InvalidOperationException("No server configured"); + } + + _encryptor = EncryptorFactory.GetEncryptor(server.method, server.password); + + _server = server; + _serverTimeout = Math.Max(1, server.timeout) * 1000; + + /* prepare address buffer length for AEAD */ + Logger.Trace($"_addrBufLength={_addrBufLength}"); + _encryptor.AddrBufLength = _addrBufLength; + } + + public void Start(byte[] firstPacket, int length) + { + _firstPacket = firstPacket; + _firstPacketLength = length; + HandshakeReceive(); + } + + private void CheckClose() + { + if (_connectionShutdown && _remoteShutdown) + { + Close(); + } + } + + private void ErrorClose(Exception e) + { + Logger.LogUsefulException(e); + Close(); + } + + public void Close() + { + lock (_closeConnLock) + { + if (_closed) + { + return; + } + + _closed = true; + } + + OnClosed?.Invoke(this, new SSRelayEventArgs(_server)); + + try + { + _connection.Shutdown(SocketShutdown.Both); + _connection.Close(); + } + catch (Exception e) + { + Logger.LogUsefulException(e); + } + + if (_currentRemoteSession != null) + { + try + { + IProxy remote = _currentRemoteSession.Remote; + remote.Shutdown(SocketShutdown.Both); + remote.Close(); + } + catch (Exception e) + { + Logger.LogUsefulException(e); + } + } + + lock (_encryptionLock) + { + lock (_decryptionLock) + { + _encryptor?.Dispose(); + } + } + } + + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } + + private void HandshakeReceive() + { + if (_closed) + { + return; + } + + try + { + int bytesRead = _firstPacketLength; + if (bytesRead > 1) + { + byte[] response = { 5, 0 }; + if (_firstPacket[0] != 5) + { + // reject socks 4 + response = new byte[] { 0, 91 }; + Logger.Error("socks 5 protocol error"); + } + _connection.BeginSend(response, 0, response.Length, SocketFlags.None, + HandshakeSendCallback, null); + } + else + { + Close(); + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void HandshakeSendCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + _connection.EndSend(ar); + + // +-----+-----+-------+------+----------+----------+ + // | VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +-----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +-----+-----+-------+------+----------+----------+ + // Skip first 3 bytes, and read 2 more bytes to analysis the address. + // 2 more bytes is designed if address is domain then we don't need to read once more to get the addr length. + // validate is unnecessary, we did it in first packet, but we can do it in future version + _connection.BeginReceive(_connetionRecvBuffer, 0, 3 + AddressTypeLength + 1, SocketFlags.None, + AddressReceiveCallback, null); + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void AddressReceiveCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + int bytesRead = _connection.EndReceive(ar); + if (bytesRead >= 5) + { + _command = _connetionRecvBuffer[1]; + switch (_command) + { + case CMD_CONNECT: + + // +----+-----+-------+------+----------+----------+ + // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + byte[] response = { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }; + _connection.BeginSend(response, 0, response.Length, SocketFlags.None, + ConnectResponseCallback, null); + break; + case CMD_UDP_ASSOC: + ReadAddress(HandleUDPAssociate); + break; + case CMD_BIND: // not implemented + default: + Logger.Debug("Unsupported CMD=" + _command); + Close(); + break; + } + } + else + { + Logger.Debug( + "failed to recv data in Shadowsocks.Controller.TCPHandler.handshakeReceive2Callback()"); + Close(); + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void ConnectResponseCallback(IAsyncResult ar) + { + try + { + _connection.EndSend(ar); + + ReadAddress(StartConnect); + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void ReadAddress(Action onSuccess) + { + int atyp = _connetionRecvBuffer[3]; + + switch (atyp) + { + case AddressTypeIPv4: // IPv4 address, 4 bytes + ReadAddress(4 + AddressPortLength - 1, onSuccess); + break; + case AddressTypeDomain: // domain name, length + str + int len = _connetionRecvBuffer[4]; + ReadAddress(len + AddressPortLength, onSuccess); + break; + case AddressTypeIPv6: // IPv6 address, 16 bytes + ReadAddress(16 + AddressPortLength - 1, onSuccess); + break; + default: + Logger.Debug("Unsupported ATYP=" + atyp); + Close(); + break; + } + } + + private void ReadAddress(int bytesRemain, Action onSuccess) + { + // drop [ VER | CMD | RSV ] + Array.Copy(_connetionRecvBuffer, 3, _connetionRecvBuffer, 0, AddressTypeLength + 1); + + // Read the remain address bytes + _connection.BeginReceive(_connetionRecvBuffer, 2, RecvSize - 2, SocketFlags.None, OnAddressFullyRead, + new object[] { bytesRemain, onSuccess }); + } + + private void OnAddressFullyRead(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + int bytesRead = _connection.EndReceive(ar); + + object[] states = (object[])ar.AsyncState; + + int bytesRemain = (int)states[0]; + Action onSuccess = (Action)states[1]; + + if (bytesRead >= bytesRemain) + { + _firstPacketLength = bytesRead + 2; + + int atyp = _connetionRecvBuffer[0]; + + string dstAddr = "Unknown"; + int dstPort = -1; + switch (atyp) + { + case AddressTypeIPv4: // IPv4 address, 4 bytes + dstAddr = new IPAddress(_connetionRecvBuffer.Skip(1).Take(4).ToArray()).ToString(); + dstPort = (_connetionRecvBuffer[5] << 8) + _connetionRecvBuffer[6]; + + _addrBufLength = AddressTypeLength + 4 + AddressPortLength; + break; + case AddressTypeDomain: // domain name, length + str + int len = _connetionRecvBuffer[1]; + dstAddr = System.Text.Encoding.UTF8.GetString(_connetionRecvBuffer, 2, len); + dstPort = (_connetionRecvBuffer[len + 2] << 8) + _connetionRecvBuffer[len + 3]; + + _addrBufLength = AddressTypeLength + 1 + len + AddressPortLength; + break; + case AddressTypeIPv6: // IPv6 address, 16 bytes + dstAddr = $"[{new IPAddress(_connetionRecvBuffer.Skip(1).Take(16).ToArray())}]"; + dstPort = (_connetionRecvBuffer[17] << 8) + _connetionRecvBuffer[18]; + + _addrBufLength = AddressTypeLength + 16 + AddressPortLength; + break; + } + + Logger.Debug($"connect to {dstAddr}:{dstPort}"); + + _destEndPoint = SocketUtil.GetEndPoint(dstAddr, dstPort); + + onSuccess.Invoke(); /* StartConnect() */ + } + else + { + Logger.Debug("failed to recv data in Shadowsocks.Controller.TCPHandler.OnAddressFullyRead()"); + Close(); + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void HandleUDPAssociate() + { + IPEndPoint endPoint = (IPEndPoint)_connection.LocalEndPoint; + byte[] address = endPoint.Address.GetAddressBytes(); + int port = endPoint.Port; + byte[] response = new byte[4 + address.Length + AddressPortLength]; + response[0] = 5; + switch (endPoint.AddressFamily) + { + case AddressFamily.InterNetwork: + response[3] = AddressTypeIPv4; + break; + case AddressFamily.InterNetworkV6: + response[3] = AddressTypeIPv6; + break; + } + address.CopyTo(response, 4); + response[response.Length - 1] = (byte)(port & 0xFF); + response[response.Length - 2] = (byte)((port >> 8) & 0xFF); + _connection.BeginSend(response, 0, response.Length, SocketFlags.None, ReadAll, true); + } + + private void ReadAll(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + if (ar.AsyncState != null) + { + _connection.EndSend(ar); + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + ReadAll, null); + } + else + { + int bytesRead = _connection.EndReceive(ar); + if (bytesRead > 0) + { + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + ReadAll, null); + } + else + { + Close(); + } + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + // inner class + private class ProxyTimer : Timer + { + public AsyncSession Session; + + public EndPoint DestEndPoint; + public Server Server; + + public ProxyTimer(int p) : base(p) + { + } + } + + private class ServerTimer : Timer + { + public AsyncSession Session; + + public Server Server; + + public ServerTimer(int p) : base(p) + { + } + } + + private void StartConnect() + { + try + { + CreateRemote(); + + // Setting up proxy + IProxy remote; + EndPoint proxyEP = null; + EndPoint serverEP = _controller.ResolveOutboundEndpoint(_server.server, _server.ServerPort); + EndPoint pluginEP = _controller.GetPluginLocalEndPointIfConfigured(_server); + + if (pluginEP != null) + { + serverEP = pluginEP; + remote = new DirectConnect(); + } + else if (_config.useProxy) + { + switch (_config.proxyType) + { + case ForwardProxyConfig.ProxySocks5: + remote = new Socks5Proxy(); + break; + case ForwardProxyConfig.ProxyHttp: + remote = new HttpProxy(); + break; + default: + throw new NotSupportedException("Unknown forward proxy."); + } + proxyEP = _controller.ResolveOutboundEndpoint(_config.proxyServer, _config.proxyPort); + } + else + { + remote = new DirectConnect(); + } + + AsyncSession session = new AsyncSession(remote); + lock (_closeConnLock) + { + if (_closed) + { + remote.Close(); + return; + } + + _currentRemoteSession = session; + } + + ProxyTimer proxyTimer = new ProxyTimer(_proxyTimeout) { AutoReset = false }; + proxyTimer.Elapsed += ProxyConnectTimer_Elapsed; + proxyTimer.Enabled = true; + + proxyTimer.Session = session; + proxyTimer.DestEndPoint = serverEP; + proxyTimer.Server = _server; + + _proxyConnected = false; + + // Connect to the proxy server. + remote.BeginConnectProxy(proxyEP, ProxyConnectCallback, + new AsyncSession(remote, proxyTimer)); + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void ProxyConnectTimer_Elapsed(object sender, ElapsedEventArgs e) + { + ProxyTimer timer = (ProxyTimer)sender; + timer.Elapsed -= ProxyConnectTimer_Elapsed; + timer.Enabled = false; + timer.Dispose(); + + + if (_proxyConnected || _destConnected || _closed) + { + return; + } + IProxy proxy = timer.Session.Remote; + + Logger.Info($"Proxy {proxy.ProxyEndPoint} timed out"); + proxy.Close(); + Close(); + } + + private void ProxyConnectCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + try + { + AsyncSession session = (AsyncSession)ar.AsyncState; + ProxyTimer timer = session.State; + EndPoint destEndPoint = timer.DestEndPoint; + Server server = timer.Server; + timer.Elapsed -= ProxyConnectTimer_Elapsed; + timer.Enabled = false; + timer.Dispose(); + + IProxy remote = session.Remote; + + // Complete the connection. + remote.EndConnectProxy(ar); + + _proxyConnected = true; + + if (!(remote is DirectConnect)) + { + Logger.Debug($"Socket connected to proxy {remote.ProxyEndPoint}"); + } + + _startConnectTime = DateTime.Now; + ServerTimer connectTimer = new ServerTimer(_serverTimeout) { AutoReset = false }; + connectTimer.Elapsed += DestConnectTimer_Elapsed; + connectTimer.Enabled = true; + connectTimer.Session = session; + connectTimer.Server = server; + + _destConnected = false; + + NetworkCredential auth = null; + if (_config.useAuth) + { + auth = new NetworkCredential(_config.authUser, _config.authPwd); + } + + // Connect to the remote endpoint. + remote.BeginConnectDest(destEndPoint, ConnectCallback, + new AsyncSession(session, connectTimer), auth); + } + catch (ArgumentException) + { + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void DestConnectTimer_Elapsed(object sender, ElapsedEventArgs e) + { + ServerTimer timer = (ServerTimer)sender; + timer.Elapsed -= DestConnectTimer_Elapsed; + timer.Enabled = false; + timer.Dispose(); + + if (_destConnected || _closed) + { + return; + } + + AsyncSession session = timer.Session; + Server server = timer.Server; + OnFailed?.Invoke(this, new SSRelayEventArgs(_server)); + Logger.Info($"{server.ToString()} timed out"); + session.Remote.Close(); + Close(); + } + + private void ConnectCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + AsyncSession session = (AsyncSession)ar.AsyncState; + ServerTimer timer = session.State; + _server = timer.Server; + timer.Elapsed -= DestConnectTimer_Elapsed; + timer.Enabled = false; + timer.Dispose(); + + IProxy remote = session.Remote; + // Complete the connection. + remote.EndConnectDest(ar); + + _destConnected = true; + + Logger.Debug($"Socket connected to ss server: {_server.ToString()}"); + + TimeSpan latency = DateTime.Now - _startConnectTime; + + OnConnected?.Invoke(this, new SSTCPConnectedEventArgs(_server, latency)); + + StartPipe(session); + } + catch (ArgumentException) + { + } + catch (Exception e) + { + if (_server != null) + { + OnFailed?.Invoke(this, new SSRelayEventArgs(_server)); + } + ErrorClose(e); + } + } + + private void TryReadAvailableData() + { + int available = Math.Min(_connection.Available, RecvSize - _firstPacketLength); + if (available > 0) + { + int size = _connection.Receive(_connetionRecvBuffer, _firstPacketLength, available, + SocketFlags.None); + + _firstPacketLength += size; + } + } + + private void StartPipe(AsyncSession session) + { + if (_closed) + { + return; + } + + try + { + _startReceivingTime = DateTime.Now; + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); + + TryReadAvailableData(); + Logger.Trace($"_firstPacketLength = {_firstPacketLength}"); + SendToServer(_firstPacketLength, session); + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void PipeRemoteReceiveCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + AsyncSession session = (AsyncSession)ar.AsyncState; + int bytesRead = session.Remote.EndReceive(ar); + _totalRead += bytesRead; + + OnInbound?.Invoke(this, new SSTransmitEventArgs(_server, bytesRead)); + if (bytesRead > 0) + { + lastActivity = DateTime.Now; + int bytesToSend = -1; + lock (_decryptionLock) + { + try + { + _encryptor.Decrypt(_remoteRecvBuffer, bytesRead, _remoteSendBuffer, out bytesToSend); + } + catch (CryptoErrorException) + { + Logger.Error("decryption error"); + Close(); + return; + } + } + if (bytesToSend == 0) + { + // need more to decrypt + Logger.Trace("Need more to decrypt"); + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); + return; + } + Logger.Trace($"start sending {bytesToSend}"); + _connection.BeginSend(_remoteSendBuffer, 0, bytesToSend, SocketFlags.None, + PipeConnectionSendCallback, new object[] { session, bytesToSend }); + } + else + { + _connection.Shutdown(SocketShutdown.Send); + _connectionShutdown = true; + CheckClose(); + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void PipeConnectionReceiveCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + int bytesRead = _connection.EndReceive(ar); + + AsyncSession session = (AsyncSession)ar.AsyncState; + IProxy remote = session.Remote; + + if (bytesRead > 0) + { + SendToServer(bytesRead, session); + } + else + { + remote.Shutdown(SocketShutdown.Send); + _remoteShutdown = true; + CheckClose(); + } + } + catch (Exception e) + { + ErrorClose(e); + } + } + + private void SendToServer(int length, AsyncSession session) + { + _totalWrite += length; + int bytesToSend; + lock (_encryptionLock) + { + try + { + _encryptor.Encrypt(_connetionRecvBuffer, length, _connetionSendBuffer, out bytesToSend); + } + catch (CryptoErrorException) + { + Logger.Debug("encryption error"); + Close(); + return; + } + } + + OnOutbound?.Invoke(this, new SSTransmitEventArgs(_server, bytesToSend)); + _startSendingTime = DateTime.Now; + session.Remote.BeginSend(_connetionSendBuffer, 0, bytesToSend, SocketFlags.None, + PipeRemoteSendCallback, new object[] { session, bytesToSend }); + } + + private void PipeRemoteSendCallback(IAsyncResult ar) + { + if (_closed) + { + return; + } + + try + { + object[] container = (object[])ar.AsyncState; + AsyncSession session = (AsyncSession)container[0]; + int bytesShouldSend = (int)container[1]; + int bytesSent = session.Remote.EndSend(ar); + + if (bytesSent > 0) + { + lastActivity = DateTime.Now; + } + + int bytesRemaining = bytesShouldSend - bytesSent; + if (bytesRemaining > 0) + { + Logger.Info("reconstruct _connetionSendBuffer to re-send"); + Buffer.BlockCopy(_connetionSendBuffer, bytesSent, _connetionSendBuffer, 0, bytesRemaining); + session.Remote.BeginSend(_connetionSendBuffer, 0, bytesRemaining, SocketFlags.None, + PipeRemoteSendCallback, new object[] { session, bytesRemaining }); + return; + } + _connection.BeginReceive(_connetionRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeConnectionReceiveCallback, session); + } + catch (Exception e) + { + ErrorClose(e); + } + } + + // In general, we assume there is no delay between local proxy and client, add this for sanity + private void PipeConnectionSendCallback(IAsyncResult ar) + { + try + { + object[] container = (object[])ar.AsyncState; + AsyncSession session = (AsyncSession)container[0]; + int bytesShouldSend = (int)container[1]; + int bytesSent = _connection.EndSend(ar); + int bytesRemaining = bytesShouldSend - bytesSent; + if (bytesRemaining > 0) + { + Logger.Info("reconstruct _remoteSendBuffer to re-send"); + Buffer.BlockCopy(_remoteSendBuffer, bytesSent, _remoteSendBuffer, 0, bytesRemaining); + _connection.BeginSend(_remoteSendBuffer, 0, bytesRemaining, SocketFlags.None, + PipeConnectionSendCallback, new object[] { session, bytesRemaining }); + return; + } + session.Remote.BeginReceive(_remoteRecvBuffer, 0, RecvSize, SocketFlags.None, + PipeRemoteReceiveCallback, session); + } + catch (Exception e) + { + ErrorClose(e); + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/UDPRelay.cs b/Shadowsocks.Windows/Controller/Service/UDPRelay.cs new file mode 100644 index 000000000..3cfdc481c --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/UDPRelay.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using NLog; +using Shadowsocks.Controller.Strategy; +using Shadowsocks.Encryption; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller +{ + internal sealed class UDPRelay : Listener.Service, IDisposable + { + private const int MaxUdpAssociations = 512; + private readonly ShadowsocksController _controller; + private readonly UdpAssociationCache _cache = new(MaxUdpAssociations); + private bool _disposed; + + public UDPRelay(ShadowsocksController controller) + { + this._controller = controller; + } + + public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) + { + if (socket.ProtocolType != ProtocolType.Udp) + { + return false; + } + if (length < 4) + { + return false; + } + Listener.UDPState udpState = (Listener.UDPState)state; + IPEndPoint remoteEndPoint = (IPEndPoint)udpState.RemoteEndPoint; + if (firstPacket[0] != 0 || firstPacket[1] != 0 || firstPacket[2] != 0) + { + return true; // SOCKS5 UDP fragmentation is intentionally unsupported; drop invalid/fragmented datagrams. + } + + EndPoint destination = TryParseDestination(firstPacket, length); + if (destination == null) + { + return true; + } + + UDPHandler handler = _cache.Get(remoteEndPoint); + if (handler == null || handler.IsClosed) + { + Server server = _controller.GetAServer(IStrategyCallerType.UDP, remoteEndPoint, destination); + if (server?.IsConfigured != true) + { + return true; + } + + handler = new UDPHandler(_controller, socket, server, remoteEndPoint); + handler.Receive(); + _cache.AddOrReplace(remoteEndPoint, handler); + } + handler.Send(firstPacket, length); + return true; + } + + public override void Shutdown() + { + Dispose(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _cache.Dispose(); + _disposed = true; + GC.SuppressFinalize(this); + } + + internal static EndPoint TryParseDestination(byte[] packet, int length) + { + if (packet == null || length < 7 || length > packet.Length) + { + return null; + } + + int offset = 3; + byte addressType = packet[offset++]; + string host; + switch (addressType) + { + case 0x01: + if (length < offset + 4 + 2) return null; + host = new IPAddress(packet.AsSpan(offset, 4)).ToString(); + offset += 4; + break; + case 0x04: + if (length < offset + 16 + 2) return null; + host = new IPAddress(packet.AsSpan(offset, 16)).ToString(); + offset += 16; + break; + case 0x03: + if (length < offset + 1) return null; + int hostLength = packet[offset++]; + if (hostLength <= 0 || length < offset + hostLength + 2) return null; + host = Encoding.ASCII.GetString(packet, offset, hostLength); + offset += hostLength; + break; + default: + return null; + } + + int port = (packet[offset] << 8) | packet[offset + 1]; + if (port <= 0) + { + return null; + } + + return IPAddress.TryParse(host, out IPAddress address) + ? new IPEndPoint(address, port) + : new DnsEndPoint(host, port); + } + + public sealed class UDPHandler : IDisposable + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + private readonly Socket _local; + private Socket _remote; + + private readonly Server _server; + private readonly byte[] _buffer = new byte[65536]; + + private readonly IPEndPoint _localEndPoint; + private readonly IPEndPoint _remoteEndPoint; + + public bool IsClosed => _remote == null; + + private static IPAddress GetBindAddress(AddressFamily addressFamily) + { + return addressFamily == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Any + : IPAddress.Any; + } + + public UDPHandler(ShadowsocksController controller, Socket local, Server server, IPEndPoint localEndPoint) + { + _local = local; + _server = server; + _localEndPoint = localEndPoint; + + _remoteEndPoint = controller.ResolveOutboundEndpoint(server.server, server.ServerPort); + _remote = new Socket(_remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + _remote.Bind(new IPEndPoint(GetBindAddress(_remote.AddressFamily), 0)); + } + + public void Send(byte[] data, int length) + { + Socket remote = _remote; + if (remote == null) + { + return; + } + + using IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); + byte[] dataIn = new byte[length - 3]; + Array.Copy(data, 3, dataIn, 0, length - 3); + byte[] dataOut = new byte[65536]; // enough space for AEAD ciphers + int outlen; + encryptor.EncryptUDP(dataIn, length - 3, dataOut, out outlen); + logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); + remote.SendTo(dataOut, outlen, SocketFlags.None, _remoteEndPoint); + } + + public void Receive() + { + Socket remote = _remote; + if (remote == null) + { + return; + } + + EndPoint remoteEndPoint = new IPEndPoint(GetBindAddress(remote.AddressFamily), 0); + logger.Debug("UDP relay waiting for server responses on {0}.", remote.LocalEndPoint); + remote.BeginReceiveFrom(_buffer, 0, _buffer.Length, 0, ref remoteEndPoint, RecvFromCallback, null); + } + + public void RecvFromCallback(IAsyncResult ar) + { + try + { + Socket remote = _remote; + if (remote == null) return; + EndPoint remoteEndPoint = new IPEndPoint(GetBindAddress(remote.AddressFamily), 0); + int bytesRead = remote.EndReceiveFrom(ar, ref remoteEndPoint); + + byte[] dataOut = new byte[bytesRead]; + int outlen; + + using IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); + encryptor.DecryptUDP(_buffer, bytesRead, dataOut, out outlen); + + byte[] sendBuf = new byte[outlen + 3]; + Array.Copy(dataOut, 0, sendBuf, 3, outlen); + + logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); + _local?.SendTo(sendBuf, outlen + 3, 0, _localEndPoint); + + Receive(); + } + catch (ObjectDisposedException) + { + Close(); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + Close(); + } + } + + public void Close() + { + Socket remote = Interlocked.Exchange(ref _remote, null); + if (remote == null) + { + return; + } + + try + { + remote.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + } + + public void Dispose() + { + Close(); + GC.SuppressFinalize(this); + } + } + } + + internal sealed class UdpAssociationCache : IDisposable + { + private readonly int _capacity; + private readonly object _sync = new(); + private readonly Dictionary> _entries = new(); + private readonly LinkedList _lru = new(); + + public UdpAssociationCache(int capacity) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + _capacity = capacity; + } + + public UDPRelay.UDPHandler Get(IPEndPoint key) + { + lock (_sync) + { + if (!_entries.TryGetValue(key, out LinkedListNode node)) + { + return null; + } + + _lru.Remove(node); + _lru.AddLast(node); + return node.Value.Handler; + } + } + + public void AddOrReplace(IPEndPoint key, UDPRelay.UDPHandler handler) + { + UDPRelay.UDPHandler toClose = null; + lock (_sync) + { + if (_entries.TryGetValue(key, out LinkedListNode existing)) + { + _lru.Remove(existing); + _entries.Remove(key); + toClose = existing.Value.Handler; + } + else if (_entries.Count >= _capacity && _lru.First is LinkedListNode oldest) + { + _lru.RemoveFirst(); + _entries.Remove(oldest.Value.Key); + toClose = oldest.Value.Handler; + } + + var entry = new Entry(key, handler); + var node = new LinkedListNode(entry); + _lru.AddLast(node); + _entries.Add(key, node); + } + + toClose?.Dispose(); + } + + public void Dispose() + { + List handlers; + lock (_sync) + { + handlers = new List(_entries.Count); + foreach (LinkedListNode node in _entries.Values) + { + handlers.Add(node.Value.Handler); + } + + _entries.Clear(); + _lru.Clear(); + } + + foreach (UDPRelay.UDPHandler handler in handlers) + { + handler.Dispose(); + } + + GC.SuppressFinalize(this); + } + + private sealed class Entry + { + public Entry(IPEndPoint key, UDPRelay.UDPHandler handler) + { + Key = key; + Handler = handler; + } + + public IPEndPoint Key { get; } + public UDPRelay.UDPHandler Handler { get; } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Service/UpdateChecker.cs b/Shadowsocks.Windows/Controller/Service/UpdateChecker.cs new file mode 100644 index 000000000..d92f4e343 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Service/UpdateChecker.cs @@ -0,0 +1,496 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NLog; +using Shadowsocks.Controller.Service; +using Shadowsocks.Core; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller +{ + public sealed class UpdateAvailableEventArgs : EventArgs + { + public UpdateAvailableEventArgs(JToken release) => Release = release; + public JToken Release { get; } + } + + public sealed class UpdateChecker + { + private static readonly SemaphoreSlim UpdateOperationLock = new(1, 1); + private readonly Logger logger = LogManager.GetCurrentClassLogger(); + private readonly HttpClient httpClient; + private readonly ShadowsocksController controller; + private readonly Version currentVersion = new(ApplicationInfo.Version); + private JToken releaseObject; + private Version releaseVersion; + + public const string PreferredReleaseZipFilename = "Shadowsocks-win-x64.zip"; + private const string UpdateUrl = ApplicationInfo.ReleasesApiUrl; + private const string GitHubApiVersion = "2026-03-10"; + private const string GitHubApiAccept = "application/vnd.github+json"; + private const string ReleaseDownloadPathPrefix = "/SlimRG/shadowsocks-reborn/releases/download/"; + + public string NewReleaseVersion { get; private set; } + public string LastCheckError { get; private set; } + + public event EventHandler CheckUpdateCompleted; + public event EventHandler UpdateAvailable; + + public UpdateChecker(ShadowsocksController controller) + { + ArgumentNullException.ThrowIfNull(controller); + this.controller = controller; + httpClient = controller.GetHttpClient(); + } + + public async Task CheckForVersionUpdate(int millisecondsDelay = 0) + { + if (millisecondsDelay > 0) + { + logger.Info("Waiting for {0}ms before checking for version update.", millisecondsDelay); + await Task.Delay(millisecondsDelay).ConfigureAwait(false); + } + + Configuration config = controller.GetCurrentConfiguration(); + LastCheckError = null; + releaseObject = null; + releaseVersion = null; + NewReleaseVersion = null; + + logger.Info("Checking GitHub Releases for an application update."); + try + { + string json = await GetReleasesJsonAsync().ConfigureAwait(false); + JArray releases = JArray.Parse(json); + Version installedVersion = ResolveEffectiveInstalledVersion(); + JToken selected = SelectLatestEligibleRelease(releases, config, installedVersion, out Version selectedVersion); + if (selected != null) + { + releaseObject = selected; + releaseVersion = selectedVersion; + NewReleaseVersion = (string)selected["tag_name"]; + logger.Info("Found application update {0}.", NewReleaseVersion); + UpdateAvailable?.Invoke(this, new UpdateAvailableEventArgs(selected)); + return; + } + + logger.Info("No newer eligible application release was found."); + CheckUpdateCompleted?.Invoke(this, EventArgs.Empty); + } + catch (Exception exception) + { + LastCheckError = exception.Message; + logger.LogUsefulException(exception); + CheckUpdateCompleted?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Downloads, verifies and stages the selected release, then starts the new executable + /// in updater mode. The caller must shut down the current application only after this + /// method returns true. + /// + public async Task DoUpdate() + { + if (releaseObject == null || releaseVersion == null) + { + throw new InvalidOperationException("No application release has been selected for update."); + } + + await UpdateOperationLock.WaitAsync().ConfigureAwait(false); + string transactionDirectory = null; + try + { + Version installedVersion = ResolveEffectiveInstalledVersion(); + if (!IsStrictlyNewerVersion(releaseVersion, installedVersion)) + { + throw new InvalidOperationException( + $"Release {releaseVersion} is not newer than the installed application version {installedVersion}."); + } + + JArray assets = releaseObject["assets"] as JArray ?? new JArray(); + JObject zipAsset = SelectReleaseZipAsset(assets) + ?? throw new InvalidDataException($"The GitHub release does not contain the required {PreferredReleaseZipFilename} asset."); + JObject checksumAsset = FindAssetByName(assets, PreferredReleaseZipFilename + ".sha256") + ?? throw new InvalidDataException($"The GitHub release does not contain the required {PreferredReleaseZipFilename}.sha256 asset."); + + transactionDirectory = SelfUpdater.CreateTransactionDirectory(); + string zipPath = Path.Combine(transactionDirectory, PreferredReleaseZipFilename); + string checksumPath = zipPath + ".sha256"; + string updaterPath = SelfUpdater.GetTemporaryUpdaterPath(transactionDirectory); + + await DownloadAssetAsync(zipAsset, zipPath).ConfigureAwait(false); + await DownloadAssetAsync(checksumAsset, checksumPath).ConfigureAwait(false); + await VerifySha256Async(zipPath, checksumPath, PreferredReleaseZipFilename).ConfigureAwait(false); + ExtractCanonicalExecutable(zipPath, updaterPath); + ValidatePayloadVersion(updaterPath, releaseVersion); + string payloadSha256 = SelfUpdater.ComputeSha256Hex(updaterPath); + + string targetExecutable = AutoStartup.GetPrimaryExecutablePath(); + _ = SelfUpdater.LaunchStagedUpdater( + updaterPath, + targetExecutable, + transactionDirectory, + payloadSha256); + transactionDirectory = null; // ownership transferred to the staged updater/new process + logger.Info("Application update handoff started for {0}.", NewReleaseVersion); + return true; + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + throw; + } + finally + { + if (!string.IsNullOrWhiteSpace(transactionDirectory)) + { + TryDeleteTransaction(transactionDirectory); + } + UpdateOperationLock.Release(); + } + } + + public void SkipUpdate() + { + if (releaseObject == null) + { + return; + } + + string version = (string)releaseObject["tag_name"] ?? string.Empty; + controller.SaveSkippedUpdateVerion(version); + logger.Info("The application update {0} has been skipped.", version); + } + + internal static JToken SelectLatestEligibleRelease( + JArray releases, + Configuration configuration, + Version installedVersion, + out Version selectedVersion) + { + selectedVersion = null; + JToken selected = null; + bool selectedPrerelease = true; + + foreach (JToken release in releases ?? new JArray()) + { + if ((bool?)release["draft"] == true) + { + continue; + } + + string tag = ((string)release["tag_name"] ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(tag) + || string.Equals(tag, configuration?.skippedUpdateVersion, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!TryParseReleaseVersion(tag, out Version parsed) + || !IsStrictlyNewerVersion(parsed, installedVersion)) + { + continue; + } + + bool prerelease = (bool?)release["prerelease"] == true; + if (prerelease && configuration?.checkPreRelease != true) + { + continue; + } + + int versionComparison = selectedVersion == null ? 1 : parsed.CompareTo(selectedVersion); + if (selected == null + || versionComparison > 0 + || (versionComparison == 0 && selectedPrerelease && !prerelease)) + { + selected = release; + selectedVersion = parsed; + selectedPrerelease = prerelease; + } + } + + return selected; + } + + internal static bool IsStrictlyNewerVersion(Version candidateVersion, Version installedVersion) + { + ArgumentNullException.ThrowIfNull(candidateVersion); + ArgumentNullException.ThrowIfNull(installedVersion); + return candidateVersion.CompareTo(installedVersion) > 0; + } + + internal static Version SelectEffectiveInstalledVersion(Version processVersion, Version primaryExecutableVersion) + { + ArgumentNullException.ThrowIfNull(processVersion); + return primaryExecutableVersion != null && primaryExecutableVersion.CompareTo(processVersion) > 0 + ? primaryExecutableVersion + : processVersion; + } + + private Version ResolveEffectiveInstalledVersion() + { + string primaryExecutable = AutoStartup.GetPrimaryExecutablePath(); + Version primaryVersion = TryReadExecutableVersion(primaryExecutable); + Version effectiveVersion = SelectEffectiveInstalledVersion(currentVersion, primaryVersion); + + if (primaryVersion != null && !effectiveVersion.Equals(currentVersion)) + { + logger.Info( + "Update check is running from version {0}, but primary executable {1} is version {2}; using {2} as the installed version.", + currentVersion, + primaryExecutable, + primaryVersion); + } + + return effectiveVersion; + } + + internal static Version TryReadExecutableVersion(string executablePath) + { + if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath)) + { + return null; + } + + try + { + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(executablePath); + return new Version( + Math.Max(0, versionInfo.FileMajorPart), + Math.Max(0, versionInfo.FileMinorPart), + Math.Max(0, versionInfo.FileBuildPart), + Math.Max(0, versionInfo.FilePrivatePart)); + } + catch + { + return null; + } + } + + internal static bool TryParseReleaseVersion(string tagName, out Version version) + { + version = null; + if (string.IsNullOrWhiteSpace(tagName)) + { + return false; + } + + string normalized = tagName.Trim(); + if (normalized.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + normalized = normalized[1..]; + } + + int suffixIndex = normalized.AsSpan().IndexOfAny('-', '+'); + if (suffixIndex >= 0) + { + normalized = normalized[..suffixIndex]; + } + + string[] parts = normalized.Split('.'); + if (parts.Length is < 2 or > 4) + { + return false; + } + + normalized = parts.Length switch + { + 2 => normalized + ".0.0", + 3 => normalized + ".0", + _ => normalized, + }; + return Version.TryParse(normalized, out version); + } + + internal static JObject SelectReleaseZipAsset(JArray assets) + => FindAssetByName(assets, PreferredReleaseZipFilename); + + internal static JObject FindAssetByName(JArray assets, string expectedName) + { + foreach (JToken token in assets ?? new JArray()) + { + if (token is JObject asset + && string.Equals((string)asset["name"], expectedName, StringComparison.Ordinal)) + { + return asset; + } + } + return null; + } + + internal static async Task VerifySha256Async(string payloadPath, string checksumPath, string expectedFileName) + { + string checksumText = (await File.ReadAllTextAsync(checksumPath).ConfigureAwait(false)).Trim(); + Match match = Regex.Match( + checksumText, + @"\A(?[0-9a-fA-F]{64})(?:[ \t]+\*?(?[^\r\n]+))?\z", + RegexOptions.CultureInvariant); + if (!match.Success) + { + throw new InvalidDataException($"Invalid SHA-256 sidecar: {Path.GetFileName(checksumPath)}."); + } + + if (match.Groups["name"].Success) + { + string sidecarFileName = match.Groups["name"].Value.Trim(); + if (!string.Equals(sidecarFileName, expectedFileName, StringComparison.Ordinal)) + { + throw new InvalidDataException( + $"SHA-256 sidecar names '{sidecarFileName}' instead of the required '{expectedFileName}'."); + } + } + + await using FileStream payload = File.OpenRead(payloadPath); + byte[] actualBytes = await SHA256.HashDataAsync(payload).ConfigureAwait(false); + string actual = Convert.ToHexString(actualBytes); + if (!string.Equals(actual, match.Groups["hash"].Value, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException($"SHA-256 verification failed for {Path.GetFileName(payloadPath)}."); + } + } + + internal static void ExtractCanonicalExecutable(string zipPath, string destinationExecutable) + { + using ZipArchive archive = ZipFile.OpenRead(zipPath); + if (archive.Entries.Count != 1) + { + throw new InvalidDataException("The application update ZIP must contain exactly one file: Shadowsocks.exe."); + } + + ZipArchiveEntry entry = archive.Entries[0]; + if (!string.Equals(entry.FullName, "Shadowsocks.exe", StringComparison.Ordinal) + || !string.Equals(entry.Name, "Shadowsocks.exe", StringComparison.Ordinal) + || entry.Length <= 0) + { + throw new InvalidDataException("The application update ZIP must contain exactly one root Shadowsocks.exe file."); + } + + string destinationDirectory = Path.GetDirectoryName(destinationExecutable) + ?? throw new InvalidOperationException("The updater destination directory is unavailable."); + Directory.CreateDirectory(destinationDirectory); + + using Stream input = entry.Open(); + using FileStream output = new(destinationExecutable, FileMode.Create, FileAccess.Write, FileShare.None); + input.CopyTo(output); + output.Flush(flushToDisk: true); + } + + internal static void ValidatePayloadVersion(string executablePath, Version expectedVersion) + { + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(executablePath); + var actualVersion = new Version( + Math.Max(0, versionInfo.FileMajorPart), + Math.Max(0, versionInfo.FileMinorPart), + Math.Max(0, versionInfo.FileBuildPart), + Math.Max(0, versionInfo.FilePrivatePart)); + + if (!actualVersion.Equals(expectedVersion)) + { + throw new InvalidDataException( + $"Update executable version {actualVersion} does not match GitHub release {expectedVersion}."); + } + } + + internal static bool IsAllowedReleaseDownloadUrl(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) + || !string.Equals(uri.Host, "github.com", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return uri.AbsolutePath.StartsWith(ReleaseDownloadPathPrefix, StringComparison.OrdinalIgnoreCase); + } + + private async Task GetReleasesJsonAsync() + { + using var request = new HttpRequestMessage(HttpMethod.Get, UpdateUrl); + request.Headers.UserAgent.ParseAdd($"Shadowsocks-Reborn/{ApplicationInfo.Version}"); + request.Headers.Accept.ParseAdd(GitHubApiAccept); + request.Headers.TryAddWithoutValidation("X-GitHub-Api-Version", GitHubApiVersion); + + using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + private async Task DownloadAssetAsync(JObject asset, string destination) + { + string url = (string)asset["browser_download_url"]; + if (!IsAllowedReleaseDownloadUrl(url)) + { + throw new InvalidDataException($"GitHub release asset '{asset["name"]}' has an invalid download URL."); + } + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.UserAgent.ParseAdd($"Shadowsocks-Reborn/{ApplicationInfo.Version}"); + using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await WriteDownloadedAssetAsync(response.Content, destination).ConfigureAwait(false); + } + + internal static async Task WriteDownloadedAssetAsync(HttpContent content, string destination) + { + ArgumentNullException.ThrowIfNull(content); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + + string partialPath = destination + ".download"; + try + { + File.Delete(partialPath); + await using (FileStream output = new(partialPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + await content.CopyToAsync(output).ConfigureAwait(false); + await output.FlushAsync().ConfigureAwait(false); + output.Flush(flushToDisk: true); + } + + // The temporary stream must be fully disposed before Windows can atomically + // promote the download to its canonical destination. + File.Move(partialPath, destination, overwrite: true); + } + finally + { + TryDeleteFile(partialPath); + } + } + + private static void TryDeleteTransaction(string directory) + { + try + { + if (SelfUpdater.IsCanonicalTransactionDirectory(directory) && Directory.Exists(directory)) + { + Directory.Delete(directory, recursive: true); + } + } + catch + { + } + } + + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.Maintenance.cs b/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.Maintenance.cs new file mode 100644 index 000000000..05ff3e75e --- /dev/null +++ b/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.Maintenance.cs @@ -0,0 +1,173 @@ +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; + +namespace Shadowsocks.Controller +{ + public sealed partial class ShadowsocksController + { + private readonly object _dnsCryptMaintenanceLock = new(); + private CancellationTokenSource? _dnsCryptMaintenanceCancellation; + private Task? _dnsCryptMaintenanceTask; + private string? _lastDnsCryptMaintenanceError; + + private void StartDnsCryptMaintenance() + { + lock (_dnsCryptMaintenanceLock) + { + if (_dnsCryptMaintenanceTask is { IsCompleted: false }) + return; + + _dnsCryptMaintenanceCancellation?.Dispose(); + _dnsCryptMaintenanceCancellation = new CancellationTokenSource(); + CancellationToken token = _dnsCryptMaintenanceCancellation.Token; + _dnsCryptMaintenanceTask = RunDnsCryptMaintenanceLoopAsync(token); + } + } + + private void StopDnsCryptMaintenance() + { + CancellationTokenSource? cancellation; + Task? task; + lock (_dnsCryptMaintenanceLock) + { + cancellation = _dnsCryptMaintenanceCancellation; + task = _dnsCryptMaintenanceTask; + _dnsCryptMaintenanceCancellation = null; + _dnsCryptMaintenanceTask = null; + } + + if (cancellation is null) + return; + + try + { + cancellation.Cancel(); + task?.GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + // Normal during Stop(), suspend, or application shutdown. + } + catch (Exception exception) + { + logger.Warn(exception, "DNSCrypt automatic maintenance did not stop cleanly."); + } + finally + { + cancellation.Dispose(); + } + } + + private async Task RunDnsCryptMaintenanceLoopAsync(CancellationToken cancellationToken) + { + try + { + await Task.Delay(DnsCryptMaintenancePolicy.StartupDelay, cancellationToken).ConfigureAwait(false); + while (!cancellationToken.IsCancellationRequested) + { + try + { + DnsCryptComponentStatus component = _dnsCryptComponentManager.GetStatus(); + DnsCryptConfig config = CloneDnsCryptConfig( + _config.dnsPolicy?.dnsCrypt ?? new DnsCryptConfig()); + + if (component.IsInstalled && component.AutoUpdate != config.autoUpdate) + { + // settings.json is the user-facing source of truth. Keep component.json + // synchronized so recovery/tools see the same preference. + _dnsCryptComponentManager.SetAutoUpdate(config.autoUpdate); + component = _dnsCryptComponentManager.GetStatus(); + } + + TimeSpan delay = DnsCryptMaintenancePolicy.GetDelayUntilNextAutomaticCheck( + component, + config, + DateTimeOffset.UtcNow); + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + continue; + } + + await _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Automatic DNSCrypt update", + AutomaticDnsCryptMaintenanceCoreAsync, + cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + // A transient local metadata/configuration fault must not permanently disable + // maintenance for the rest of this application session. Surface the error and + // retry only after the local idle interval; no GitHub request is made here. + _lastDnsCryptMaintenanceError = exception.Message; + logger.Warn(exception, "DNSCrypt automatic maintenance iteration failed."); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + await Task.Delay( + DnsCryptMaintenancePolicy.IdleRecheckInterval, + cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Normal lifecycle cancellation. + } + } + + private async Task AutomaticDnsCryptMaintenanceCoreAsync(CancellationToken cancellationToken) + { + DnsCryptComponentStatus before = _dnsCryptComponentManager.GetStatus(); + DnsCryptConfig config = CloneDnsCryptConfig( + _config.dnsPolicy?.dnsCrypt ?? new DnsCryptConfig()); + DateTimeOffset nowUtc = DateTimeOffset.UtcNow; + + if (!DnsCryptMaintenancePolicy.IsAutomaticCheckDue(before, config, nowUtc)) + return; + + // Persist the attempt before touching the network. A failed/cancelled automatic + // request therefore cannot create a retry storm across resume or process restart; + // manual Check for Update remains available at any time. + _dnsCryptComponentManager.RecordUpdateCheck(nowUtc); + try + { + DnsCryptReleaseInfo release = await _dnsCryptComponentManager + .GetLatestReleaseAsync(cancellationToken) + .ConfigureAwait(false); + _latestDnsCryptVersion = release.Version; + _lastDnsCryptMaintenanceError = null; + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + + if (before.ActiveVersion is not null && release.Version > before.ActiveVersion) + { + logger.Info( + "DNSCryptProxy | UPDATE | Automatic update {0} -> {1} is available.", + before.ActiveVersion, + release.Version); + await UpdateDnsCryptReleaseCoreAsync( + before, + release, + progress: null, + cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + _lastDnsCryptMaintenanceError = exception.Message; + logger.Warn(exception, "DNSCrypt automatic update check/update failed."); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.cs b/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.cs new file mode 100644 index 000000000..5a632b367 --- /dev/null +++ b/Shadowsocks.Windows/Controller/ShadowsocksController.DnsCrypt.cs @@ -0,0 +1,1428 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Core; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller +{ + public sealed partial class ShadowsocksController + { + + + private void RememberAutomaticDnsCryptRuntimeResolver() + { + if (_config.dnsPolicy?.dnsCrypt?.automaticResolvers != true) + return; + + IReadOnlyList runtimeNames = _dnsCryptRuntimeManager.GetActiveResolverNames(); + if (runtimeNames.Count > 0) + _dnsCryptAutomaticServerNames = runtimeNames.ToArray(); + } + + public DnsCryptManagementStatus GetDnsCryptManagementStatus() + { + DnsCryptComponentStatus component = _dnsCryptComponentManager.GetStatus(); + DnsCryptRuntimeStatus runtime = _dnsCryptRuntimeManager.GetStatus(); + DnsCryptConfig config = _config.dnsPolicy?.dnsCrypt ?? new DnsCryptConfig(); + bool automaticResolvers = config.automaticResolvers; + IReadOnlyList runtimeNames = _dnsCryptRuntimeManager.GetActiveResolverNames(); + IReadOnlyList activeNames = automaticResolvers + ? (runtimeNames.Count > 0 ? runtimeNames : _dnsCryptAutomaticServerNames) + : config.serverNames; + IReadOnlyDictionary runtimeLatencies = _dnsCryptRuntimeManager.GetResolverLatencies(); + DnsCryptResolverInfo[] activeResolvers = activeNames + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => + { + DnsCryptResolverInfo resolver = _dnsCryptResolverCatalog.FirstOrDefault( + item => string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)) + ?? CreateResolverPlaceholder(name); + return runtimeLatencies.TryGetValue(name, out int latencyMs) + ? resolver with { LatencyMs = latencyMs } + : resolver; + }) + .ToArray(); + return new DnsCryptManagementStatus( + _config.dnsPolicy?.mode ?? DnsPolicyMode.System, + component, + runtime, + _latestDnsCryptVersion, + _lastDnsCryptMaintenanceError, + automaticResolvers, + activeResolvers); + } + + public Task CheckDnsCryptUpdateAsync(CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Check DNSCrypt update", + CheckDnsCryptUpdateCoreAsync, + cancellationToken); + } + + private async Task CheckDnsCryptUpdateCoreAsync(CancellationToken cancellationToken) + { + DnsCryptReleaseInfo release = await _dnsCryptComponentManager.GetLatestReleaseAsync(cancellationToken).ConfigureAwait(false); + _latestDnsCryptVersion = release.Version; + _lastDnsCryptMaintenanceError = null; + _dnsCryptComponentManager.RecordUpdateCheck(DateTimeOffset.UtcNow); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + return release; + } + + public Task> GetDnsCryptResolversAsync(CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "List DNSCrypt resolvers", + GetDnsCryptResolversCoreAsync, + cancellationToken); + } + + private async Task> GetDnsCryptResolversCoreAsync(CancellationToken cancellationToken) + { + DnsCryptComponentStatus component = _dnsCryptComponentManager.GetStatus(); + if (!component.IsInstalled) + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + + DnsCryptConfig config = CloneDnsCryptConfig(_config.dnsPolicy?.dnsCrypt ?? new DnsCryptConfig()); + // -list-all reads the signed resolver catalog. Manual mode intentionally exposes + // DNSCrypt and DoH entries; ODoH remains excluded. + config.serverNames = []; + config.automaticResolvers = false; + config.routeThroughShadowsocks = false; + IReadOnlyList resolvers = await GetAndCacheDnsCryptResolversAsync(config, cancellationToken).ConfigureAwait(false); + return resolvers.Where(IsSupportedDnsCryptProxyResolver).ToArray(); + } + + public async Task> ProbeDnsCryptResolverLatenciesAsync( + IEnumerable resolverNames, + CancellationToken cancellationToken = default) + { + string[] names = (resolverNames ?? Array.Empty()) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + if (names.Length == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + IReadOnlyList catalog = _dnsCryptResolverCatalog; + DnsCryptResolverInfo[] targets = catalog + .Where(resolver => names.Contains(resolver.Name, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + IReadOnlyDictionary measured = await DnsCryptResolverLatencyProbe + .ProbeAsync(targets, cancellationToken) + .ConfigureAwait(false); + if (measured.Count == 0) + return measured; + + IReadOnlyDictionary runtimeLatencies = _dnsCryptRuntimeManager.GetResolverLatencies(); + _dnsCryptResolverCatalog = _dnsCryptResolverCatalog + .Select(resolver => runtimeLatencies.TryGetValue(resolver.Name, out int runtimeLatency) + ? resolver with { LatencyMs = runtimeLatency } + : measured.TryGetValue(resolver.Name, out int latencyMs) + ? resolver with { LatencyMs = latencyMs } + : resolver) + .ToArray(); + // The DNS page updates its local list directly from this return value. Raising the + // global status event for every background latency batch caused duplicate page refreshes. + return measured; + } + + public Task InstallDnsCryptAsync( + IProgress progress = null, + CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Install DNSCrypt", + token => InstallDnsCryptCoreAsync(progress, token), + cancellationToken); + } + + private async Task InstallDnsCryptCoreAsync( + IProgress progress, + CancellationToken cancellationToken) + { + DnsCryptComponentStatus existing = _dnsCryptComponentManager.GetStatus(); + if (existing.IsInstalled) + return GetDnsCryptManagementStatus(); + + DnsCryptPreparedComponent prepared = null; + DnsCryptActivationLease activation = null; + bool activated = false; + try + { + prepared = await _dnsCryptComponentManager.PrepareLatestAsync(progress, cancellationToken: cancellationToken).ConfigureAwait(false); + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.ValidatingRuntime)); + await _dnsCryptRuntimeManager.ValidatePreparedAsync( + prepared, CreateDnsCryptRuntimeOptions(_config.dnsPolicy?.dnsCrypt), cancellationToken).ConfigureAwait(false); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Activating)); + activation = _dnsCryptComponentManager.ActivatePrepared(prepared); + activated = true; + _dnsCryptComponentManager.SetAutoUpdate(_config.dnsPolicy?.dnsCrypt?.autoUpdate ?? true); + if (IsDnsCryptRuntimeRequired()) + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Starting)); + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy.dnsCrypt, cancellationToken).ConfigureAwait(false); + } + _dnsCryptComponentManager.CommitActivation(activation); + _latestDnsCryptVersion = prepared.Version; + _dnsCryptComponentManager.RecordUpdateCheck(DateTimeOffset.UtcNow); + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Ready)); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + return GetDnsCryptManagementStatus(); + } + catch (Exception exception) + { + logger.Error(exception, "DNSCrypt Proxy installation failed."); + if (activation is not null && !activation.Completed) + { + try + { + _dnsCryptComponentManager.RollbackActivation(activation); + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "Failed to roll back DNSCrypt Proxy activation after install failure."); + } + } + else if (prepared is not null && !activated) + { + _dnsCryptComponentManager.DiscardPrepared(prepared); + } + throw; + } + } + + public Task UpdateDnsCryptAsync( + IProgress progress = null, + CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Update DNSCrypt", + token => UpdateDnsCryptCoreAsync(progress, token), + cancellationToken); + } + + private async Task UpdateDnsCryptCoreAsync( + IProgress progress, + CancellationToken cancellationToken) + { + DnsCryptComponentStatus before = _dnsCryptComponentManager.GetStatus(); + if (!before.IsInstalled) + return await InstallDnsCryptCoreAsync(progress, cancellationToken).ConfigureAwait(false); + + DnsCryptReleaseInfo release = await CheckDnsCryptUpdateCoreAsync(cancellationToken).ConfigureAwait(false); + return await UpdateDnsCryptReleaseCoreAsync(before, release, progress, cancellationToken).ConfigureAwait(false); + } + + private async Task UpdateDnsCryptReleaseCoreAsync( + DnsCryptComponentStatus before, + DnsCryptReleaseInfo release, + IProgress progress, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(before); + ArgumentNullException.ThrowIfNull(release); + if (!before.IsInstalled || before.ActiveVersion is null) + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + + if (release.Version <= before.ActiveVersion) + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Ready)); + return GetDnsCryptManagementStatus(); + } + + DnsCryptPreparedComponent prepared = null; + DnsCryptActivationLease activation = null; + bool activated = false; + DnsCryptRuntimeStatus runtimeBefore = _dnsCryptRuntimeManager.GetStatus(); + bool wasRunning = runtimeBefore.IsRunning && IsDnsCryptRuntimeRequired(); + if (!IsDnsCryptRuntimeRequired() && runtimeBefore.IsServing) + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + + // Validate the complete launch topology before publishing the Updating state. If this + // throws, the runtime state remains untouched. + DnsCryptRuntimeStartOptions options = CreateDnsCryptRuntimeOptions(_config.dnsPolicy?.dnsCrypt); + DnsCryptRuntimeStatus updateSnapshot = _dnsCryptRuntimeManager.BeginUpdate(); + bool updateSucceeded = false; + try + { + // Use the exact release object that was already selected and version-checked. + // This avoids a second GitHub "latest" request changing underneath the + // transaction and keeps automatic checks to their 24-hour cadence. + prepared = await _dnsCryptComponentManager + .PrepareReleaseAsync(release, progress, cancellationToken: cancellationToken) + .ConfigureAwait(false); + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.ValidatingRuntime)); + await _dnsCryptRuntimeManager.ValidatePreparedAsync(prepared, options, cancellationToken).ConfigureAwait(false); + + if (wasRunning) + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Activating)); + activation = _dnsCryptComponentManager.ActivatePrepared(prepared); + activated = true; + if (wasRunning || IsDnsCryptRuntimeRequired()) + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Starting)); + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, cancellationToken).ConfigureAwait(false); + } + + _dnsCryptComponentManager.CommitActivation(activation); + _latestDnsCryptVersion = prepared.Version; + _lastDnsCryptMaintenanceError = null; + updateSucceeded = true; + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Ready)); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + return GetDnsCryptManagementStatus(); + } + catch (Exception exception) + { + logger.Error(exception, "DNSCrypt Proxy update failed."); + bool componentRolledBack = false; + if (activation is not null && !activation.Completed) + { + try + { + _dnsCryptComponentManager.RollbackActivation(activation); + componentRolledBack = true; + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "DNSCrypt Proxy activation rollback failed after update failure."); + } + } + else if (prepared is not null && !activated) + { + _dnsCryptComponentManager.DiscardPrepared(prepared); + } + else if (prepared is not null) + { + DnsCryptComponentStatus current = _dnsCryptComponentManager.GetStatus(); + if (current.ActiveVersion == prepared.Version && current.PreviousVersion is not null) + { + try + { + _dnsCryptComponentManager.Rollback(); + componentRolledBack = true; + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "DNSCrypt Proxy rollback failed after update failure."); + } + } + } + + if (componentRolledBack && (wasRunning || IsDnsCryptRuntimeRequired()) + && !_dnsCryptRuntimeManager.GetStatus().IsServing) + { + try + { + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception restartException) + { + logger.Error(restartException, "Failed to restart the previous DNSCrypt Proxy after update rollback."); + } + } + + if (wasRunning && !_dnsCryptRuntimeManager.GetStatus().IsServing) + { + try + { + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception restartException) + { + logger.Error(restartException, "Failed to restart DNSCrypt Proxy after update failure."); + } + } + + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + throw; + } + finally + { + _dnsCryptRuntimeManager.EndUpdate(updateSnapshot, updateSucceeded); + } + } + + public Task ReinstallDnsCryptAsync( + IProgress progress = null, + CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Reinstall DNSCrypt", + token => ReinstallDnsCryptCoreAsync(progress, token), + cancellationToken); + } + + private async Task ReinstallDnsCryptCoreAsync( + IProgress progress, + CancellationToken cancellationToken) + { + DnsCryptComponentStatus before = _dnsCryptComponentManager.GetStatus(); + if (!before.IsInstalled) + return await InstallDnsCryptCoreAsync(progress, cancellationToken).ConfigureAwait(false); + + DnsCryptRuntimeStatus runtimeBefore = _dnsCryptRuntimeManager.GetStatus(); + bool wasRunning = runtimeBefore.IsRunning && IsDnsCryptRuntimeRequired(); + if (!IsDnsCryptRuntimeRequired() && runtimeBefore.IsServing) + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + + // Validate before BeginUpdate() so invalid bootstrap/settings never leave the runtime + // presentation stuck in Updating. + DnsCryptRuntimeStartOptions options = CreateDnsCryptRuntimeOptions(_config.dnsPolicy?.dnsCrypt); + DnsCryptRuntimeStatus updateSnapshot = _dnsCryptRuntimeManager.BeginUpdate(); + bool updateSucceeded = false; + DnsCryptPreparedComponent prepared = null; + DnsCryptActivationLease activation = null; + bool activated = false; + try + { + // Keep the active runtime serving DNS while the replacement is downloaded and + // validated on its isolated loopback port. Downtime begins only at activation. + prepared = await _dnsCryptComponentManager.PrepareLatestAsync( + progress, forceDownload: true, cancellationToken: cancellationToken).ConfigureAwait(false); + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.ValidatingRuntime)); + await _dnsCryptRuntimeManager.ValidatePreparedAsync(prepared, options, cancellationToken).ConfigureAwait(false); + + if (wasRunning) + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Activating)); + activation = _dnsCryptComponentManager.ActivatePrepared(prepared); + activated = true; + if (wasRunning || IsDnsCryptRuntimeRequired()) + { + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Starting)); + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, cancellationToken).ConfigureAwait(false); + } + _dnsCryptComponentManager.CommitActivation(activation); + _latestDnsCryptVersion = prepared.Version; + _dnsCryptComponentManager.RecordUpdateCheck(DateTimeOffset.UtcNow); + updateSucceeded = true; + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Ready)); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + return GetDnsCryptManagementStatus(); + } + catch (Exception exception) + { + logger.Error(exception, "DNSCrypt Proxy reinstall failed."); + bool componentRolledBack = false; + if (activation is not null && !activation.Completed) + { + try + { + _dnsCryptComponentManager.RollbackActivation(activation); + componentRolledBack = true; + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "Failed to roll back DNSCrypt Proxy activation after reinstall failure."); + } + } + else if (prepared is not null && !activated) + { + _dnsCryptComponentManager.DiscardPrepared(prepared); + } + else if (activated && prepared is not null && prepared.Version != before.ActiveVersion) + { + try + { + DnsCryptComponentStatus current = _dnsCryptComponentManager.GetStatus(); + if (current.PreviousVersion is not null) + { + _dnsCryptComponentManager.Rollback(); + componentRolledBack = true; + } + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "Failed to roll back DNSCrypt Proxy after reinstall failure."); + } + } + + if (componentRolledBack && (wasRunning || IsDnsCryptRuntimeRequired()) + && !_dnsCryptRuntimeManager.GetStatus().IsServing) + { + try + { + await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception restartException) + { + logger.Error(restartException, "Failed to restart the previous DNSCrypt Proxy after reinstall rollback."); + } + } + + if (wasRunning && !_dnsCryptRuntimeManager.GetStatus().IsServing) + { + try { await StartDnsCryptWithResolvedResolversAsync( + _config.dnsPolicy?.dnsCrypt, CancellationToken.None).ConfigureAwait(false); } + catch (Exception restartException) { logger.Error(restartException, "Failed to restart DNSCrypt Proxy after reinstall failure."); } + } + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + throw; + } + finally + { + _dnsCryptRuntimeManager.EndUpdate(updateSnapshot, updateSucceeded); + } + } + + private void EnsureConfiguredServerForDnsPolicy() + { + if (!_config.HasConfiguredServer) + { + throw new InvalidOperationException( + "A configured Shadowsocks server is required before enabling a non-system DNS policy."); + } + } + + public Task SetDnsPolicyAsync(DnsPolicyMode mode, CancellationToken cancellationToken = default) + { + if (mode != DnsPolicyMode.System && !_config.HasConfiguredServer) + { + return Task.FromException(new InvalidOperationException( + "A configured Shadowsocks server is required before enabling a non-system DNS policy.")); + } + + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Set DNS policy", + token => SetDnsPolicyCoreAsync(mode, token), + cancellationToken); + } + + private async Task SetDnsPolicyCoreAsync(DnsPolicyMode mode, CancellationToken cancellationToken) + { + _config.dnsPolicy ??= new DnsPolicyConfig(); + _config.dnsPolicy.dnsCrypt ??= new DnsCryptConfig(); + DnsPolicyMode previous = _config.dnsPolicy.mode; + bool startedForNewPolicy = false; + + try + { + if (mode == DnsPolicyMode.DnsCrypt) + { + DnsCryptComponentStatus component = _dnsCryptComponentManager.GetStatus(); + if (!component.IsInstalled) + throw new InvalidOperationException("DNSCrypt Proxy is not installed."); + + // Resolve and pin the signed resolver catalog while the previous DNS policy + // is still active. Catalog maintenance resolves source hosts with Cloudflare DoH + // over the local Shadowsocks tunnel and falls back to Google DoH. It must finish + // before DNSCrypt fail-closed capture is armed. The active runtime itself receives + // only local [static.*] stamps and performs no source refresh or plaintext DNS. + DnsCryptRuntimeStartOptions startupOptions = null; + if (!_dnsCryptRuntimeManager.GetStatus().IsServing) + { + startupOptions = await CreateDnsCryptRuntimeOptionsForStartAsync( + _config.dnsPolicy.dnsCrypt, cancellationToken).ConfigureAwait(false); + } + + // Start and health-check the pinned runtime while the previous DNS policy is + // still active. Only after it is serving do we arm DNSCrypt fail-closed capture. + // This avoids both bootstrap deadlocks and an unnecessary DNS outage during start. + if (!_dnsCryptRuntimeManager.GetStatus().IsServing) + { + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + await _dnsCryptRuntimeManager.StartAsync( + startupOptions ?? throw new InvalidOperationException("DNSCrypt startup options were not prepared."), + cancellationToken).ConfigureAwait(false); + if (_config.dnsPolicy.dnsCrypt.automaticResolvers) + RememberAutomaticDnsCryptRuntimeResolver(); + startedForNewPolicy = true; + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + } + } + + _config.dnsPolicy.mode = mode; + _trafficPolicyEngine.UpdateConfiguration(_config); + Configuration.Save(_config); + + // Entering DNSCrypt: runtime is healthy before Admin redirect is enabled. + // Leaving DNSCrypt: Admin redirect is disabled before the runtime stops. + await ApplyTrafficCaptureConfigurationCoreAsync( + _config, + ensureDnsRuntime: false, + cancellationToken: cancellationToken).ConfigureAwait(false); + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + + + ConfigChanged?.Invoke(this, EventArgs.Empty); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + } + catch + { + _config.dnsPolicy.mode = previous; + _trafficPolicyEngine.UpdateConfiguration(_config); + Configuration.Save(_config); + try + { + await ApplyTrafficCaptureConfigurationCoreAsync( + _config, + ensureDnsRuntime: false, + cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + catch (Exception restoreException) + { + logger.Warn(restoreException, "Failed to restore previous DNS capture policy."); + } + + if (startedForNewPolicy && previous != DnsPolicyMode.DnsCrypt) + { + try { await _dnsCryptRuntimeManager.StopAsync(CancellationToken.None).ConfigureAwait(false); } + catch (Exception stopException) { logger.Warn(stopException, "Failed to stop DNSCrypt after DNS policy rollback."); } + } + throw; + } + } + + public Task SetDirectDnsPolicyAsync(string serverAddress, CancellationToken cancellationToken = default) + => SetDirectDnsPolicyAsync(serverAddress, string.Empty, _config.dnsPolicy?.directDnsRouteThroughShadowsocks ?? false, cancellationToken); + + public Task SetDirectDnsPolicyAsync( + string primaryServerAddress, + string fallbackServerAddress, + CancellationToken cancellationToken = default) + => SetDirectDnsPolicyAsync( + primaryServerAddress, + fallbackServerAddress, + _config.dnsPolicy?.directDnsRouteThroughShadowsocks ?? false, + cancellationToken); + + public Task SetDirectDnsPolicyAsync( + string primaryServerAddress, + string fallbackServerAddress, + bool routeThroughShadowsocks, + CancellationToken cancellationToken = default) + { + EnsureConfiguredServerForDnsPolicy(); + string primary = NormalizeDirectDnsServer(primaryServerAddress, nameof(primaryServerAddress)); + string fallback = NormalizeDirectDnsServer(fallbackServerAddress, nameof(fallbackServerAddress)); + if (primary.Length == 0 && fallback.Length > 0) + throw new ArgumentException("A primary DNS server is required when a fallback DNS server is configured.", nameof(primaryServerAddress)); + if (string.Equals(primary, fallback, StringComparison.OrdinalIgnoreCase)) + fallback = string.Empty; + + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Set direct DNS policy", + token => SetDirectDnsPolicyCoreAsync(primary, fallback, routeThroughShadowsocks, token), + cancellationToken); + } + + private async Task SetDirectDnsPolicyCoreAsync( + string primaryServerAddress, + string fallbackServerAddress, + bool routeThroughShadowsocks, + CancellationToken cancellationToken) + { + _config.dnsPolicy ??= new DnsPolicyConfig(); + string previousPrimary = _config.dnsPolicy.directDnsServer; + string previousFallback = _config.dnsPolicy.directDnsFallbackServer; + bool previousRoute = _config.dnsPolicy.directDnsRouteThroughShadowsocks; + _config.dnsPolicy.directDnsServer = primaryServerAddress; + _config.dnsPolicy.directDnsFallbackServer = fallbackServerAddress; + _config.dnsPolicy.directDnsRouteThroughShadowsocks = routeThroughShadowsocks; + try + { + await SetDnsPolicyCoreAsync(DnsPolicyMode.Direct, cancellationToken).ConfigureAwait(false); + } + catch + { + _config.dnsPolicy.directDnsServer = previousPrimary; + _config.dnsPolicy.directDnsFallbackServer = previousFallback; + _config.dnsPolicy.directDnsRouteThroughShadowsocks = previousRoute; + Configuration.Save(_config); + throw; + } + } + + private static string NormalizeDirectDnsServer(string serverAddress, string parameterName) + { + string value = serverAddress?.Trim().TrimStart('[').TrimEnd(']') ?? string.Empty; + if (value.Length == 0) + return string.Empty; + if (!IPAddress.TryParse(value, out IPAddress address)) + throw new ArgumentException("DNS server address must be an IPv4 or IPv6 address.", parameterName); + return address.ToString(); + } + + public Task SetCustomDohPolicyAsync(string url, CancellationToken cancellationToken = default) + => SetCustomDohPolicyAsync( + url, + _config.dnsPolicy?.customDohRouteThroughShadowsocks ?? false, + cancellationToken); + + public Task SetCustomDohPolicyAsync( + string url, + bool routeThroughShadowsocks, + CancellationToken cancellationToken = default) + { + EnsureConfiguredServerForDnsPolicy(); + if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out Uri uri) + || !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("Custom DoH URL must be an absolute HTTPS URL.", nameof(url)); + } + + string normalized = uri.AbsoluteUri; + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Set custom DoH policy", + token => SetCustomDohPolicyCoreAsync(normalized, routeThroughShadowsocks, token), + cancellationToken); + } + + private async Task SetCustomDohPolicyCoreAsync( + string url, + bool routeThroughShadowsocks, + CancellationToken cancellationToken) + { + _config.dnsPolicy ??= new DnsPolicyConfig(); + string previousUrl = _config.dnsPolicy.customDohUrl; + bool previousRoute = _config.dnsPolicy.customDohRouteThroughShadowsocks; + _config.dnsPolicy.customDohUrl = url; + _config.dnsPolicy.customDohRouteThroughShadowsocks = routeThroughShadowsocks; + try + { + await SetDnsPolicyCoreAsync(DnsPolicyMode.CustomDoh, cancellationToken).ConfigureAwait(false); + } + catch + { + _config.dnsPolicy.customDohUrl = previousUrl; + _config.dnsPolicy.customDohRouteThroughShadowsocks = previousRoute; + Configuration.Save(_config); + throw; + } + } + + public Task SaveDnsCryptSettingsAsync(DnsCryptConfig config, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(config); + DnsCryptConfig snapshot = CloneDnsCryptConfig(config); + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Save DNSCrypt settings", + token => SaveDnsCryptSettingsCoreAsync(snapshot, token), + cancellationToken); + } + + private async Task SaveDnsCryptSettingsCoreAsync(DnsCryptConfig config, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(config); + DnsCryptConfig next = await SanitizeDnsCryptConfigForSupportedResolversAsync(config, cancellationToken).ConfigureAwait(false); + // Validate bootstrap topology even in User Mode, where no runtime is started. This + // prevents persisting a configuration that can only fail when Admin Mode is enabled. + DnsCryptBootstrapPolicy.Validate(_config, next); + + if (!next.ipv4Servers && !next.ipv6Servers) + throw new ArgumentException("At least one DNSCrypt address family must be enabled.", nameof(config)); + + _config.dnsPolicy ??= new DnsPolicyConfig(); + DnsCryptConfig previous = CloneDnsCryptConfig(_config.dnsPolicy.dnsCrypt ?? new DnsCryptConfig()); + bool wasRunning = IsDnsCryptRuntimeRequired() && _dnsCryptRuntimeManager.GetStatus().IsRunning; + // Resolve Automatic mode against the signed catalog before persistence when the + // managed component is available, so impossible filter combinations are rejected + // immediately instead of failing on the next start. Settings can still be prepared + // before installation; install/start performs the same filtered selection later. + bool componentInstalled = _dnsCryptComponentManager.GetStatus().IsInstalled; + DnsCryptRuntimeStartOptions nextOptions = componentInstalled + ? await CreateDnsCryptRuntimeOptionsForStartAsync(next, cancellationToken).ConfigureAwait(false) + : CreateDnsCryptRuntimeOptions(next); + if (componentInstalled && !next.automaticResolvers) + { + await _dnsCryptRuntimeManager.ValidateSettingsAsync( + nextOptions, + TimeSpan.FromSeconds(8), + cancellationToken).ConfigureAwait(false); + } + + if (wasRunning) + { + try + { + using var applyTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + applyTimeout.CancelAfter(TimeSpan.FromSeconds(next.automaticResolvers ? 25 : 12)); + // Both modes start from a fully resolved, bounded server set. Automatic + // mode has already applied DNSSEC/no-log/no-filter/address-family constraints. + await _dnsCryptRuntimeManager.StartAsync(nextOptions, applyTimeout.Token).ConfigureAwait(false); + if (next.automaticResolvers) + RememberAutomaticDnsCryptRuntimeResolver(); + } + catch (Exception applyException) + { + string upstreamError = _dnsCryptRuntimeManager.GetLastRuntimeUpstreamError(); + try + { + using var rollbackTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(12)); + if (previous.automaticResolvers) + { + await StartDnsCryptWithResolvedResolversAsync(previous, rollbackTimeout.Token).ConfigureAwait(false); + } + else + { + await _dnsCryptRuntimeManager.StartAsync( + await CreateDnsCryptRuntimeOptionsForStartAsync(previous, rollbackTimeout.Token).ConfigureAwait(false), + rollbackTimeout.Token).ConfigureAwait(false); + } + } + catch (Exception rollbackException) + { + logger.Error(rollbackException, "Failed to restore DNSCrypt runtime after settings apply failed."); + } + + if (applyException is OperationCanceledException) + { + string message = "DNSCrypt settings could not be activated before the operation timeout; the previous runtime was restored."; + if (!string.IsNullOrWhiteSpace(upstreamError)) + message += $" Last dnscrypt-proxy error: {upstreamError}"; + throw new TimeoutException(message, applyException); + } + throw; + } + } + + _config.dnsPolicy.dnsCrypt = next; + _dnsCryptComponentManager.SetAutoUpdate(next.autoUpdate); + Configuration.Save(_config); + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + if (_config.trafficCaptureMode == TrafficCaptureMode.Admin && _adminCaptureManager.IsBrokerRunning) + { + await ApplyTrafficCaptureConfigurationCoreAsync( + _config, + ensureDnsRuntime: false, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + ConfigChanged?.Invoke(this, EventArgs.Empty); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + } + + public Task RestartDnsCryptAsync(CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Restart DNSCrypt", + RestartDnsCryptCoreAsync, + cancellationToken); + } + + private Task RestartDnsCryptCoreAsync(CancellationToken cancellationToken) + { + if (!IsDnsCryptRuntimeRequired()) + throw new InvalidOperationException("DNSCrypt runtime is active only while DNSCrypt mode is selected."); + + return RestartDnsCryptWithAutomaticResolversAsync(cancellationToken); + } + + public Task RemoveDnsCryptAsync( + IProgress progress = null, + CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Remove DNSCrypt", + token => RemoveDnsCryptCoreAsync(progress, token), + cancellationToken); + } + + private async Task RemoveDnsCryptCoreAsync( + IProgress progress, + CancellationToken cancellationToken) + { + if (_config.dnsPolicy?.mode == DnsPolicyMode.DnsCrypt) + { + await SetDnsPolicyCoreAsync(DnsPolicyMode.System, cancellationToken).ConfigureAwait(false); + } + else + { + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Removing)); + _dnsCryptComponentManager.Remove(); + try + { + if (Directory.Exists(Shadowsocks.Core.Storage.AppStoragePaths.DnsCryptRuntimeDirectory)) + Directory.Delete(Shadowsocks.Core.Storage.AppStoragePaths.DnsCryptRuntimeDirectory, recursive: true); + } + catch (Exception exception) + { + logger.Warn(exception, "Failed to remove DNSCrypt runtime directory."); + } + _latestDnsCryptVersion = null; + progress?.Report(new DnsCryptComponentProgress(DnsCryptComponentStage.Ready)); + ConfigChanged?.Invoke(this, EventArgs.Empty); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + } + + private bool IsDnsCryptRuntimeRequired() + { + return _config.HasConfiguredServer + && ShouldRunDnsCryptRuntime( + _config.trafficCaptureMode, + _config.dnsPolicy?.mode ?? DnsPolicyMode.System); + } + + internal static bool ShouldRunDnsCryptRuntime( + TrafficCaptureMode captureMode, + DnsPolicyMode dnsMode) + { + _ = captureMode; // capture mode controls coverage, not DNSCrypt process lifetime. + return dnsMode == DnsPolicyMode.DnsCrypt; + } + + internal IPEndPoint ResolveOutboundEndpoint(string host, int port) + { + if (port is < 1 or > 65535) + throw new ArgumentOutOfRangeException(nameof(port)); + if (string.IsNullOrWhiteSpace(host)) + throw new ArgumentException("An outbound host is required.", nameof(host)); + string normalizedHost = host.Trim().TrimStart('[').TrimEnd(']'); + if (IPAddress.TryParse(normalizedHost, out IPAddress parsed)) + return new IPEndPoint(parsed, port); + + if (_config.dnsPolicy?.mode == DnsPolicyMode.DnsCrypt) + { + DnsCryptRuntimeStatus runtime = _dnsCryptRuntimeManager.GetStatus(); + if (!runtime.IsServing) + { + throw new InvalidOperationException( + $"DNSCrypt is selected but unavailable; refusing plaintext system-DNS resolution for '{normalizedHost}'."); + } + + IReadOnlyList resolved = _dnsCryptRuntimeManager + .ResolveHostAsync(normalizedHost) + .GetAwaiter() + .GetResult(); + IPAddress address = resolved.FirstOrDefault(candidate => candidate.AddressFamily == AddressFamily.InterNetwork) + ?? (resolved.Count > 0 + ? resolved[0] + : throw new SocketException((int)SocketError.HostNotFound)); + return new IPEndPoint(address, port); + } + + IPAddress[] systemAddresses = Dns.GetHostAddresses(normalizedHost); + IPAddress selected = systemAddresses.FirstOrDefault(candidate => candidate.AddressFamily == AddressFamily.InterNetwork) + ?? (systemAddresses.Length > 0 + ? systemAddresses[0] + : throw new SocketException((int)SocketError.HostNotFound)); + return new IPEndPoint(selected, port); + } + +#nullable enable + public static Task GetServerCountryAsync(Server server, CancellationToken cancellationToken = default) + { + if (server is null || string.IsNullOrWhiteSpace(server.server)) + return Task.FromResult(null); + return IpCountryService.ResolveHostAsync(server.server, cancellationToken); + } +#nullable restore + + public Task TestDnsCryptAsync(CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Test DNSCrypt", + token => _dnsCryptRuntimeManager.HealthCheckAsync(token), + cancellationToken); + } + + public Task TestDnsPrivacyAsync(CancellationToken cancellationToken = default) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Test DNS privacy", + TestDnsPrivacyCoreAsync, + cancellationToken); + } + + private async Task TestDnsPrivacyCoreAsync(CancellationToken cancellationToken) + { + DnsCryptManagementStatus management = GetDnsCryptManagementStatus(); + TrafficCaptureStatus traffic = GetTrafficCaptureStatus(); + DnsCryptConfig config = _config.dnsPolicy?.dnsCrypt ?? new DnsCryptConfig(); + + bool runtimeHealthy = management.Runtime.IsServing + && await _dnsCryptRuntimeManager.HealthCheckAsync(cancellationToken).ConfigureAwait(false); + bool adminInterception = traffic.RuntimeMode == TrafficRuntimeMode.Admin + && traffic.WinDivertActive + && traffic.DnsInterceptionActive; + // Administrator-mode DNSCrypt interception is an effective fail-closed + // security invariant even when an older persisted preference contains false. + bool plaintextFallbackBlocked = (_config.dnsPolicy?.mode ?? DnsPolicyMode.System) == DnsPolicyMode.DnsCrypt + || config.failClosed; + + bool bootstrapDisabled = false; + bool systemDnsIgnored = false; + bool automaticResolverPinned = !config.automaticResolvers; + if (!string.IsNullOrWhiteSpace(management.Runtime.ConfigPath) + && File.Exists(management.Runtime.ConfigPath)) + { + string toml = await File.ReadAllTextAsync(management.Runtime.ConfigPath, cancellationToken).ConfigureAwait(false); + bootstrapDisabled = !DnsCryptTomlGenerator.RuntimeUsesPlaintextBootstrap(toml) + && !toml.Contains("[sources.public-resolvers]", StringComparison.Ordinal); + systemDnsIgnored = toml.Contains("ignore_system_dns = true", StringComparison.Ordinal); + if (config.automaticResolvers) + { + automaticResolverPinned = toml.Contains("server_names = [", StringComparison.Ordinal) + && toml.Contains("[static.'", StringComparison.Ordinal) + && toml.Contains("stamp = 'sdns://", StringComparison.Ordinal) + && !toml.Contains("[sources.public-resolvers]", StringComparison.Ordinal) + && !toml.Contains("9.9.9.11:53", StringComparison.Ordinal) + && !toml.Contains("8.8.8.8:53", StringComparison.Ordinal); + } + } + + IReadOnlyList activeResolvers = management.ActiveResolvers ?? Array.Empty(); + string transport = string.Join( + ", ", + activeResolvers + .Select(resolver => resolver.Protocol?.Trim()) + .Where(protocol => !string.IsNullOrWhiteSpace(protocol)) + .Distinct(StringComparer.OrdinalIgnoreCase)); + if (string.IsNullOrWhiteSpace(transport)) + transport = "—"; + + string upstream = string.Join( + ", ", + activeResolvers.Select(resolver => resolver.Name).Where(name => !string.IsNullOrWhiteSpace(name))); + if (string.IsNullOrWhiteSpace(upstream)) + upstream = "—"; + + var failures = new List(); + if ((_config.dnsPolicy?.mode ?? DnsPolicyMode.System) != DnsPolicyMode.DnsCrypt) + failures.Add("DNSCrypt mode is not selected."); + if (!runtimeHealthy) + failures.Add("DNSCrypt runtime health-check failed."); + if (!adminInterception) + failures.Add("Administrator DNS interception is not active."); + if (!plaintextFallbackBlocked) + failures.Add("Plaintext DNS fallback is not blocked."); + if (!bootstrapDisabled) + failures.Add("The active DNS runtime still permits plaintext bootstrap DNS or remote resolver-source refresh."); + if (!systemDnsIgnored) + failures.Add("The active DNS runtime does not ignore system DNS."); + if (!automaticResolverPinned) + failures.Add("Automatic mode is not pinned to a filtered resolver set from the signed catalog."); + + return new DnsPrivacySelfTestResult( + failures.Count == 0, + runtimeHealthy, + adminInterception, + plaintextFallbackBlocked, + bootstrapDisabled, + systemDnsIgnored, + automaticResolverPinned, + config.routeThroughShadowsocks, + upstream, + transport, + failures); + } + + private async Task StartDnsCryptWithResolvedResolversAsync( + DnsCryptConfig config, CancellationToken cancellationToken) + { + DnsCryptConfig source = config ?? new DnsCryptConfig(); + DnsCryptRuntimeStartOptions options = await CreateDnsCryptRuntimeOptionsForStartAsync( + source, cancellationToken).ConfigureAwait(false); + + // Automatic mode resolves a concrete filtered resolver set before runtime start. + // dnscrypt-proxy remains constrained to that set and never falls back to an + // unrelated provider when the selected automatic resolver is unavailable. + DnsCryptRuntimeStatus status = await _dnsCryptRuntimeManager.StartAsync( + options, cancellationToken).ConfigureAwait(false); + if (source.automaticResolvers) + RememberAutomaticDnsCryptRuntimeResolver(); + return status; + } + + private Task RestartDnsCryptWithAutomaticResolversAsync(CancellationToken cancellationToken) + => StartDnsCryptWithResolvedResolversAsync(_config.dnsPolicy?.dnsCrypt, cancellationToken); + +#nullable enable annotations + private async Task> GetAndCacheDnsCryptResolversAsync( + DnsCryptConfig config, CancellationToken cancellationToken) + { + if (_dnsCryptResolverCatalog.Length > 0 + && DateTimeOffset.UtcNow - _dnsCryptResolverCatalogLoadedUtc < TimeSpan.FromHours(6)) + return _dnsCryptResolverCatalog; + + IReadOnlyList resolvers; + try + { + resolvers = await _dnsCryptRuntimeManager + .ListResolversAsync(CreateDnsCryptRuntimeOptions(config), cancellationToken) + .ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + logger.Warn(exception, "DNSCrypt signed resolver catalog could not be refreshed or loaded."); + throw new DnsCryptBootstrapException( + "DNSCrypt could not refresh or load the signed resolver catalog.", + exception); + } + resolvers = await EnrichResolverCountriesAsync(resolvers, cancellationToken).ConfigureAwait(false); + IReadOnlyDictionary runtimeLatencies = _dnsCryptRuntimeManager.GetResolverLatencies(); + DnsCryptResolverInfo[] enrichedResolvers = resolvers + .Select(resolver => runtimeLatencies.TryGetValue(resolver.Name, out int latencyMs) + ? resolver with { LatencyMs = latencyMs } + : resolver) + .ToArray(); + _dnsCryptResolverCatalog = enrichedResolvers; + _dnsCryptResolverCatalogLoadedUtc = DateTimeOffset.UtcNow; + return enrichedResolvers; + } + + private async Task> EnrichResolverCountriesAsync( + IReadOnlyList resolvers, + CancellationToken cancellationToken) + { + if (resolvers.Count == 0) + return resolvers; + + // dnscrypt-proxy -list-all exposes resolver endpoint addresses. Resolve their + // actual IPs and use GeoIP as the sole source of country metadata. Resolver + // names/descriptions are deliberately ignored: city/provider naming is not + // authoritative geography and Anycast is not a country. + var resolverIps = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var allIps = new List(); + using var catalogDohResolver = new ShadowsocksDohResolver(_config.LocalHost, _config.localPort); + + foreach (DnsCryptResolverInfo resolver in resolvers) + { + cancellationToken.ThrowIfCancellationRequested(); + var literalAddresses = new List(); + var hostNames = new List(); + foreach (string endpoint in resolver.Addresses ?? Array.Empty()) + { + string host = IpCountryService.NormalizeHost(endpoint); + if (string.IsNullOrWhiteSpace(host)) + continue; + + if (System.Net.IPAddress.TryParse(host, out System.Net.IPAddress? ip)) + { + if (IpCountryService.IsPublicAddress(ip)) + literalAddresses.Add(ip); + } + else + { + hostNames.Add(host); + } + } + + var resolvedHostAddresses = new List(); + if (literalAddresses.Count == 0) + { + foreach (string host in hostNames.Distinct(StringComparer.OrdinalIgnoreCase)) + { + try + { + System.Net.IPAddress[] resolved = await catalogDohResolver + .ResolveAsync(host, cancellationToken) + .ConfigureAwait(false); + resolvedHostAddresses.AddRange(resolved.Where(IpCountryService.IsPublicAddress)); + } + catch (Exception exception) when (!cancellationToken.IsCancellationRequested) + { + logger.Debug(exception, $"DNSCrypt resolver endpoint could not be resolved for GeoIP: {host}"); + } + } + } + + // A literal address from the DNS stamp is the authoritative endpoint. + // Hostname resolution is used only when the stamp has no fixed address. + System.Net.IPAddress[] unique = literalAddresses + .Concat(resolvedHostAddresses) + .Distinct() + .ToArray(); + resolverIps[resolver.Name] = unique; + allIps.AddRange(unique); + } + + IReadOnlyDictionary countries = await IpCountryService + .LookupAddressesAsync(allIps.Distinct(), cancellationToken) + .ConfigureAwait(false); + + return resolvers.Select(resolver => + { + if (!resolverIps.TryGetValue(resolver.Name, out IReadOnlyList? ips)) + return resolver with { CountryCode = string.Empty, CountryName = string.Empty, FlagEmoji = string.Empty }; + + IpCountryInfo[] located = ips + .Select(ip => countries.TryGetValue(ip.ToString(), out IpCountryInfo? country) ? country : null) + .Where(country => country is not null) + .Cast() + .ToArray(); + string[] countryCodes = located + .Select(country => country.CountryCode) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + // A resolver with endpoints geolocated to different countries has no + // single country. Do not choose one arbitrarily just to fill the UI. + if (countryCodes.Length != 1) + return resolver with { CountryCode = string.Empty, CountryName = string.Empty, FlagEmoji = string.Empty }; + + IpCountryInfo country = located.First(item => + string.Equals(item.CountryCode, countryCodes[0], StringComparison.OrdinalIgnoreCase)); + return resolver with + { + CountryCode = country.CountryCode, + CountryName = country.CountryName, + FlagEmoji = country.FlagEmoji, + }; + }).ToArray(); + } + +#nullable restore annotations + + private async Task CreateDnsCryptRuntimeOptionsForStartAsync( + DnsCryptConfig config, CancellationToken cancellationToken) + { + DnsCryptConfig snapshot = CloneDnsCryptConfig(config ?? new DnsCryptConfig()); + IReadOnlyList catalog; + if (snapshot.automaticResolvers) + { + var listingConfig = CloneDnsCryptConfig(snapshot); + listingConfig.serverNames = []; + listingConfig.routeThroughShadowsocks = false; + catalog = await GetAndCacheDnsCryptResolversAsync( + listingConfig, cancellationToken).ConfigureAwait(false); + IReadOnlyList targetCountries = await GetDnsCryptTargetCountriesAsync( + cancellationToken).ConfigureAwait(false); + IReadOnlyList selected = targetCountries.Count > 0 + ? await DnsCryptCountrySelector.SelectAsync( + catalog, + targetCountries, + snapshot, + IpCountryService.ResolveHostAsync, + cancellationToken).ConfigureAwait(false) + : Array.Empty(); + if (selected.Count == 0) + selected = DnsCryptCountrySelector.SelectFallback(catalog, snapshot); + if (selected.Count == 0) + { + throw new InvalidOperationException( + "No DNSCrypt/DoH resolver matches the selected automatic filters (DNSSEC, logging, filtering and address family)." + ); + } + + snapshot.serverNames = selected.ToList(); + _dnsCryptAutomaticServerNames = snapshot.serverNames.ToArray(); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + logger.Info( + "DNSCrypt automatic resolver selection: {0} (DNSSEC={1}, NoLog={2}, NoFilter={3}, IPv4={4}, IPv6={5}).", + string.Join(", ", snapshot.serverNames), + snapshot.requireDnssec, + snapshot.requireNoLog, + snapshot.requireNoFilter, + snapshot.ipv4Servers, + snapshot.ipv6Servers); + } + else + { + _dnsCryptAutomaticServerNames = Array.Empty(); + List originalNames = snapshot.serverNames.ToList(); + snapshot = await SanitizeDnsCryptConfigForSupportedResolversAsync(snapshot, cancellationToken).ConfigureAwait(false); + if (!originalNames.SequenceEqual(snapshot.serverNames, StringComparer.OrdinalIgnoreCase) + && _config.dnsPolicy?.dnsCrypt is not null) + { + _config.dnsPolicy.dnsCrypt.serverNames = snapshot.serverNames.ToList(); + Configuration.Save(_config); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + } + + var listingConfig = CloneDnsCryptConfig(snapshot); + listingConfig.serverNames = []; + listingConfig.routeThroughShadowsocks = false; + catalog = await GetAndCacheDnsCryptResolversAsync(listingConfig, cancellationToken).ConfigureAwait(false); + } + + if (snapshot.routeThroughShadowsocks) + DnsCryptBootstrapPolicy.Validate(_config, snapshot); + + Dictionary stamps = DnsCryptRuntimeManager.BuildStaticResolverStampMap( + catalog, snapshot.serverNames); + return new DnsCryptRuntimeStartOptions(snapshot, _config.localPort) + { + ShadowsocksSocks5Host = _config.LocalHost, + StaticResolverStamps = stamps, + }; + } + + private async Task> GetDnsCryptTargetCountriesAsync(CancellationToken cancellationToken) + { + IEnumerable servers = !string.IsNullOrWhiteSpace(_config.strategy) + ? _config.configs?.Where(server => server?.IsConfigured == true) ?? [] + : _config.GetCurrentServer() is Server current && current.IsConfigured ? [current] : []; + + var countries = new List(); + foreach (Server server in servers.GroupBy(item => item.server, StringComparer.OrdinalIgnoreCase).Select(group => group.First())) + { + cancellationToken.ThrowIfCancellationRequested(); + IpCountryInfo country = await IpCountryService.ResolveHostAsync(server.server, cancellationToken).ConfigureAwait(false); + if (country is not null && !countries.Any(item => string.Equals(item.CountryCode, country.CountryCode, StringComparison.OrdinalIgnoreCase))) + countries.Add(country); + } + return countries; + } + + private void QueueDnsCryptAutomaticResolverRefresh() + { + if (_config.dnsPolicy?.dnsCrypt?.automaticResolvers != true || !IsDnsCryptRuntimeRequired()) + return; + + _ = Task.Run(async () => + { + try + { + await _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Refresh automatic DNSCrypt resolver", + RestartDnsCryptWithAutomaticResolversAsync, + CancellationToken.None).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.Warn(exception, "Automatic DNSCrypt resolver refresh failed after the active Shadowsocks server changed."); + } + }); + } + + private DnsCryptRuntimeStartOptions CreateDnsCryptRuntimeOptions(DnsCryptConfig config) + { + DnsCryptConfig snapshot = CloneDnsCryptConfig(config ?? new DnsCryptConfig()); + if (snapshot.routeThroughShadowsocks) + DnsCryptBootstrapPolicy.Validate(_config, snapshot); + return new DnsCryptRuntimeStartOptions(snapshot, _config.localPort) + { + ShadowsocksSocks5Host = _config.LocalHost, + }; + } + + private async Task SanitizeDnsCryptConfigForSupportedResolversAsync( + DnsCryptConfig source, + CancellationToken cancellationToken) + { + DnsCryptConfig snapshot = CloneDnsCryptConfig(source ?? new DnsCryptConfig()); + if (snapshot.automaticResolvers) + { + // Persisted Automatic mode keeps serverNames empty. Runtime startup derives + // a fresh resolver set from the signed catalog and the persisted filters. + snapshot.serverNames = []; + return snapshot; + } + if (snapshot.serverNames.Count == 0) + throw new ArgumentException("Manual DNSCrypt resolver selection requires at least one resolver.", nameof(source)); + + IReadOnlyList catalog = _dnsCryptResolverCatalog; + if (catalog.Count == 0) + { + try + { + var listingConfig = CloneDnsCryptConfig(snapshot); + listingConfig.serverNames = []; + listingConfig.routeThroughShadowsocks = false; + catalog = await GetAndCacheDnsCryptResolversAsync(listingConfig, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + // ODoH is hard-disabled in generated TOML. Preserve names while the signed + // catalog is temporarily unavailable instead of silently changing Manual mode; + // candidate validation still proves the selected runtime before persistence. + logger.Warn(exception, "Could not verify manual resolver names against the signed catalog."); + return snapshot; + } + } + + var supportedNames = catalog + .Where(IsSupportedDnsCryptProxyResolver) + .Select(resolver => resolver.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + List sanitized = snapshot.serverNames + .Where(supportedNames.Contains) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + if (sanitized.Count != snapshot.serverNames.Count) + logger.Warn("Removed unsupported or unknown resolver names from the DNSCrypt configuration."); + if (sanitized.Count == 0) + throw new ArgumentException("Manual resolver selection must contain at least one DNSCrypt or DoH resolver.", nameof(source)); + snapshot.serverNames = sanitized; + return snapshot; + } + + private static bool IsSupportedDnsCryptProxyResolver(DnsCryptResolverInfo resolver) + { + if (resolver is null || string.IsNullOrWhiteSpace(resolver.Protocol)) + return false; + + return resolver.Protocol.Contains("DNSCrypt", StringComparison.OrdinalIgnoreCase) + || string.Equals(resolver.Protocol.Trim(), "DoH", StringComparison.OrdinalIgnoreCase); + } + + private static DnsCryptResolverInfo CreateResolverPlaceholder(string name) + { + // Runtime can report an active server name before the signed catalog has been loaded + // into the UI cache. Do not infer protocol, address family or privacy capabilities from + // provider/server names; those properties are authoritative only when catalog metadata + // is available. + return new DnsCryptResolverInfo( + name, + string.Empty, + false, + null, + false, + false, + string.Empty, + Array.Empty()); + } + + private static DnsCryptConfig CloneDnsCryptConfig(DnsCryptConfig source) + { + source ??= new DnsCryptConfig(); + return new DnsCryptConfig + { + autoUpdate = source.autoUpdate, + requireDnssec = source.requireDnssec, + requireNoLog = source.requireNoLog, + requireNoFilter = source.requireNoFilter, + ipv4Servers = source.ipv4Servers, + ipv6Servers = source.ipv6Servers, + routeThroughShadowsocks = source.routeThroughShadowsocks, + automaticResolvers = source.automaticResolvers, + failClosed = source.failClosed, + serverNames = source.serverNames? + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(name => name.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList() ?? [], + }; + } + + } +} diff --git a/Shadowsocks.Windows/Controller/ShadowsocksController.Plugins.Maintenance.cs b/Shadowsocks.Windows/Controller/ShadowsocksController.Plugins.Maintenance.cs new file mode 100644 index 000000000..cb6168e13 --- /dev/null +++ b/Shadowsocks.Windows/Controller/ShadowsocksController.Plugins.Maintenance.cs @@ -0,0 +1,128 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Shadowsocks.Controller.Service; + +namespace Shadowsocks.Controller +{ + public sealed partial class ShadowsocksController + { + private static readonly TimeSpan PluginMaintenanceStartupDelay = TimeSpan.FromSeconds(20); + private static readonly TimeSpan PluginMaintenancePollInterval = TimeSpan.FromHours(1); + private readonly object _pluginMaintenanceLock = new(); + private CancellationTokenSource? _pluginMaintenanceCancellation; + private Task? _pluginMaintenanceTask; + + public Task CheckPluginUpdatesAsync( + bool force = false, + CancellationToken cancellationToken = default) + => PluginManager.UpdateCatalogPluginsAsync(GetPluginIdsInUse(), force, cancellationToken); + + private void StartPluginMaintenance() + { + lock (_pluginMaintenanceLock) + { + if (_pluginMaintenanceTask is { IsCompleted: false }) + return; + + _pluginMaintenanceCancellation?.Dispose(); + _pluginMaintenanceCancellation = new CancellationTokenSource(); + CancellationToken token = _pluginMaintenanceCancellation.Token; + _pluginMaintenanceTask = RunPluginMaintenanceLoopAsync(token); + } + } + + private void StopPluginMaintenance() + { + CancellationTokenSource? cancellation; + Task? task; + lock (_pluginMaintenanceLock) + { + cancellation = _pluginMaintenanceCancellation; + task = _pluginMaintenanceTask; + _pluginMaintenanceCancellation = null; + _pluginMaintenanceTask = null; + } + + if (cancellation is null) + return; + + try + { + cancellation.Cancel(); + task?.GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + // Normal during Stop(), suspend, or application shutdown. + } + catch (Exception exception) + { + logger.Warn(exception, "SIP003 plugin automatic maintenance did not stop cleanly."); + } + finally + { + cancellation.Dispose(); + } + } + + private async Task RunPluginMaintenanceLoopAsync(CancellationToken cancellationToken) + { + try + { + await Task.Delay(PluginMaintenanceStartupDelay, cancellationToken).ConfigureAwait(false); + while (!cancellationToken.IsCancellationRequested) + { + try + { + PluginUpdateSummary summary = await CheckPluginUpdatesAsync( + force: false, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (summary.UpdatedCount > 0) + { + logger.Info( + "SIP003 plugins | UPDATE | Automatically updated {0} plugin(s).", + summary.UpdatedCount); + } + + if (summary.SkippedInUseCount > 0) + { + logger.Debug( + "SIP003 plugins | UPDATE | Deferred {0} plugin(s) because they are active in this session.", + summary.SkippedInUseCount); + } + + foreach (string error in summary.Errors) + logger.Warn("SIP003 plugins | UPDATE | {0}", error); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception exception) + { + logger.Warn(exception, "SIP003 plugin automatic maintenance iteration failed."); + } + + await Task.Delay(PluginMaintenancePollInterval, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Normal lifecycle cancellation. + } + } + + private string[] GetPluginIdsInUse() + => _pluginsByServer.Keys + .Select(server => server?.plugin) + .Where(plugin => !string.IsNullOrWhiteSpace(plugin)) + .Select(plugin => plugin!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } +} diff --git a/Shadowsocks.Windows/Controller/ShadowsocksController.cs b/Shadowsocks.Windows/Controller/ShadowsocksController.cs new file mode 100644 index 000000000..ecd64d0bb --- /dev/null +++ b/Shadowsocks.Windows/Controller/ShadowsocksController.cs @@ -0,0 +1,1727 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Core.Logging; +using Shadowsocks.Controller.Service; +using Shadowsocks.Controller.Strategy; +using Shadowsocks.Controller.Traffic; +using Shadowsocks.Model; +using Shadowsocks.Core; +using Shadowsocks.Routing; + +namespace Shadowsocks.Controller +{ + public sealed partial class ShadowsocksController : IDisposable + { + private readonly Logger logger; + private readonly HttpClient httpClient; + private readonly IUserInteractionService userInteraction; + + // Application controller: coordinates configuration, proxy services and routing. + // User-facing operations are delegated through IUserInteractionService. + #region Members definition + private readonly object _trafficStatisticsSync = new(); + private CancellationTokenSource _trafficStatisticsCancellation; + private Task _trafficStatisticsTask; + private Queue _trafficPerSecondQueue; + + private Listener _listener; + private PACDaemon _pacDaemon; + private PACServer _pacServer; + private Configuration _config; + private StrategyManager _strategyManager; + private readonly TrafficPolicyEngine _trafficPolicyEngine; + private readonly AdminCaptureManager _adminCaptureManager; + private readonly GameModeManager _gameModeManager; + private readonly DnsCryptComponentManager _dnsCryptComponentManager; + private readonly DnsCryptRuntimeManager _dnsCryptRuntimeManager; + private readonly DnsCryptCoordinator _dnsCryptCoordinator; + private DnsCryptResolverInfo[] _dnsCryptResolverCatalog = Array.Empty(); + private string[] _dnsCryptAutomaticServerNames = Array.Empty(); + private DateTimeOffset _dnsCryptResolverCatalogLoadedUtc; + private readonly SemaphoreSlim _dnsCaptureRefreshGate = new(1, 1); + private readonly SemaphoreSlim _managedRoutingCaptureRefreshGate = new(1, 1); + private int _managedRoutingCaptureRefreshPending; + private string _managedRoutingCaptureRefreshTrigger = string.Empty; + private int _suppressDnsCaptureRefresh; + private Version _latestDnsCryptVersion; + private ManagedHttpProxyService _managedHttpProxy; + private readonly ConcurrentDictionary _pluginsByServer; + private Exception _lastListenerError; + + private long _inboundCounter; + private long _outboundCounter; + public long InboundCounter => Interlocked.Read(ref _inboundCounter); + public long OutboundCounter => Interlocked.Read(ref _outboundCounter); + private bool stopped; + private bool _disposed; + private long _trafficConfigurationGeneration; + + public class PathEventArgs : EventArgs + { + public string Path { get; set; } + } + + public sealed class TrafficPerSecond + { + public long InboundCounter { get; init; } + public long OutboundCounter { get; init; } + public long InboundIncrement { get; init; } + public long OutboundIncrement { get; init; } + } + + public event EventHandler ConfigChanged; + public event EventHandler EnableStatusChanged; + public event EventHandler EnableGlobalChanged; + public event EventHandler ShareOverLANStatusChanged; + public event EventHandler VerboseLoggingStatusChanged; + public event EventHandler ShowPluginOutputChanged; + public event EventHandler ShowDnsLogsChanged; + public event EventHandler TrafficChanged; + public event EventHandler TrafficModeChanged; + public event EventHandler DnsCryptStatusChanged; + + public event EventHandler UserRuleFileReadyToOpen; + + public event EventHandler UpdatePACFromGeositeCompleted; + + public event ErrorEventHandler UpdatePACFromGeositeError; + + public event ErrorEventHandler Errored; + + #endregion + + public ShadowsocksController(IUserInteractionService userInterAction = null) + { + this.userInteraction = userInterAction ?? NullUserInteractionService.Instance; + logger = LogManager.GetCurrentClassLogger(); + httpClient = new HttpClient(); + _config = Configuration.Load(); + Configuration.Process(ref _config); + _strategyManager = new StrategyManager(() => _config); + _trafficPolicyEngine = new TrafficPolicyEngine(_config); + _adminCaptureManager = new AdminCaptureManager(); + _gameModeManager = new GameModeManager(_adminCaptureManager); + _gameModeManager.StatusChanged += (_, _) => TrafficModeChanged?.Invoke(this, EventArgs.Empty); + _dnsCryptComponentManager = new DnsCryptComponentManager( + () => _config.LocalHost, + () => _config.localPort); + _dnsCryptRuntimeManager = new DnsCryptRuntimeManager( + _dnsCryptComponentManager, + () => _config?.showDnsLogs == true, + () => _config?.isVerboseLogging == true); + _dnsCryptCoordinator = new DnsCryptCoordinator(); + _dnsCryptRuntimeManager.StatusChanged += (_, _) => + { + RememberAutomaticDnsCryptRuntimeResolver(); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + if (Volatile.Read(ref _suppressDnsCaptureRefresh) == 0) + _ = RefreshDnsCaptureAfterRuntimeChangeAsync(); + }; + _dnsCryptRuntimeManager.ResolverMetricsChanged += (_, _) => + { + RememberAutomaticDnsCryptRuntimeResolver(); + DnsCryptStatusChanged?.Invoke(this, EventArgs.Empty); + }; + _pluginsByServer = new ConcurrentDictionary(ReferenceEqualityComparer.Instance); + StartTrafficStatistics(61); + + } + + #region Basic + + public Exception LastListenerError => _lastListenerError; + public bool IsProxyListenerRunning => _listener is not null && _lastListenerError is null; + + public void Start(bool systemWakeUp = false) + { + _dnsCryptCoordinator.Resume(); + stopped = false; + if (_config.firstRunOnNewVersion && !systemWakeUp) + { + string previousVersion = string.IsNullOrWhiteSpace(_config.version) ? "unknown" : _config.version; + logger.Info("Updated from {0} to {1}", previousVersion, ApplicationInfo.Version); + // Delete pac.txt when regeneratePacOnUpdate is true. + if (_config.regeneratePacOnUpdate) + try + { + File.Delete(PACDaemon.PacFile); + logger.Info("Deleted pac.txt from previous version."); + } + catch (Exception e) + { + logger.LogUsefulException(e); + } + // finish up first run of new version + _config.firstRunOnNewVersion = false; + _config.version = ApplicationInfo.Version; + Configuration.Save(_config); + } + Reload(); + StartDnsCryptMaintenance(); + StartPluginMaintenance(); + } + + public void Stop() + { + if (stopped) + { + return; + } + stopped = true; + // Stop background component maintenance before tearing down capture/runtime. + StopPluginMaintenance(); + + // Cancel DNSCrypt downloads/update validation before tearing down capture/runtime. + // This is also used by Windows suspend, so no maintenance task keeps Clean Mode + // files or runtime processes alive across Stop()/Start(). + StopDnsCryptMaintenance(); + + // A manual install/update/settings transaction can still be running while the app + // exits or Windows suspends. Cancel it, wait for the shared DNS transaction gate, + // then tear capture down before the local DNS listener. + try + { + _dnsCryptCoordinator.SuspendAndExecuteAsync( + "Stop DNSCrypt subsystem", + async cancellationToken => + { + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + try + { + await _gameModeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + + try + { + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + }).GetAwaiter().GetResult(); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + Listener listener = _listener; + _listener = null; + listener?.Dispose(); + + GeositeUpdater.UpdateCompleted -= PacServer_PACUpdateCompleted; + GeositeUpdater.Error -= PacServer_PACUpdateError; + if (_pacDaemon != null) + { + _pacDaemon.PACFileChanged -= PacDaemon_PACFileChanged; + _pacDaemon.UserRuleFileChanged -= PacDaemon_UserRuleFileChanged; + _pacDaemon.Dispose(); + _pacDaemon = null; + } + _pacServer = null; + + StopPlugins(); + if (_config.Enabled) + { + SystemProxy.Update(_config, true, null, userInteraction); + } + Encryption.RNG.Close(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + try + { + Stop(); + } + finally + { + _disposed = true; + StopTrafficStatistics(); + _gameModeManager.DisposeAsync().AsTask().GetAwaiter().GetResult(); + _adminCaptureManager.DisposeAsync().AsTask().GetAwaiter().GetResult(); + _dnsCryptRuntimeManager.Dispose(); + _dnsCryptComponentManager.Dispose(); + _dnsCryptCoordinator.Dispose(); + _dnsCaptureRefreshGate.Dispose(); + _managedRoutingCaptureRefreshGate.Dispose(); + httpClient.Dispose(); + GC.SuppressFinalize(this); + } + } + + private void Reload() + { + Encryption.RNG.Reload(); + // some logic in configuration updated the config when saving, we need to read it again + _config = Configuration.Load(); + Configuration.Process(ref _config); + long trafficConfigurationGeneration = Interlocked.Increment(ref _trafficConfigurationGeneration); + if (!_config.HasConfiguredServer) + { + logger.Warn( + "No Shadowsocks server is configured. Proxy relay will stay inactive until a server is added. " + + "Settings are stored under the active application data root."); + } + if (!_config.useOnlinePac) + GeositeUpdater.ConfigureSources(_config.geositeUrls); + + LoggingConfigurator.Configure(_config.isVerboseLogging); + + // set User-Agent for httpClient + try + { + if (!string.IsNullOrWhiteSpace(_config.userAgentString)) + httpClient.DefaultRequestHeaders.Add("User-Agent", _config.userAgentString); + } + catch + { + // reset userAgent to default and reapply + Configuration.ResetUserAgent(_config); + httpClient.DefaultRequestHeaders.Add("User-Agent", _config.userAgentString); + } + + if (_pacDaemon == null) + { + _pacDaemon = new PACDaemon(_config); + _pacDaemon.PACFileChanged += PacDaemon_PACFileChanged; + _pacDaemon.UserRuleFileChanged += PacDaemon_UserRuleFileChanged; + } + else + { + PACDaemon.UpdateConfiguration(_config); + } + + _pacServer = _pacServer ?? new PACServer(_pacDaemon); + _pacServer.UpdatePACURL(_config); // Both Local and cached Online PAC are served from localhost. + + // Do not even initialize GeoSite while Online PAC is selected. + if (!_config.useOnlinePac) + { + GeositeUpdater.ResetEvent(); + GeositeUpdater.UpdateCompleted += PacServer_PACUpdateCompleted; + GeositeUpdater.Error += PacServer_PACUpdateError; + } + + _listener?.Stop(); + StopPlugins(); + _lastListenerError = null; + + try + { + var strategy = GetCurrentStrategy(); + strategy?.ReloadServers(); + + StartPlugin(); + _trafficPolicyEngine.UpdateConfiguration(_config); + RefreshManagedRoutingSnapshot("configuration reload"); + _managedHttpProxy = new ManagedHttpProxyService(_trafficPolicyEngine, _config); + + List services = new List + { + _pacServer, + _managedHttpProxy + }; + + if (_config.HasConfiguredServer) + { + TCPRelay tcpRelay = new TCPRelay(this, _config); + tcpRelay.OnInbound += UpdateInboundCounter; + tcpRelay.OnOutbound += UpdateOutboundCounter; + tcpRelay.OnFailed += (o, e) => GetCurrentStrategy()?.SetFailure(e.Server); + + services.Insert(0, new UDPRelay(this)); + services.Insert(0, tcpRelay); + } + _listener = new Listener(services); + _listener.Start(_config); + _ = ApplyTrafficCaptureAsync(_config, trafficConfigurationGeneration); + } + catch (Exception e) + { + // translate Microsoft language into human language + // i.e. An attempt was made to access a socket in a way forbidden by its access permissions => Port already in use + if (e is SocketException se) + { + if (se.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + e = new InvalidOperationException(I18N.GetString("Port {0} already in use", _config.localPort), e); + } + else if (se.SocketErrorCode == SocketError.AccessDenied) + { + e = new InvalidOperationException(I18N.GetString("Port {0} is reserved by system", _config.localPort), e); + } + } + _lastListenerError = e; + logger.LogUsefulException(e); + ReportError(e); + } + + // Apply the Windows proxy first, then notify the UI. Both PAC modes now point + // WinINet at localhost; remote data is refreshed only after the SS listener is up. + UpdateSystemProxy(); + ConfigChanged?.Invoke(this, new EventArgs()); + + _ = RefreshActivePacDataAsync(_config); + } + + private void SaveConfig(Configuration newConfig) + { + Configuration.Save(newConfig); + Reload(); + } + + private void ReportError(Exception e) + { + Errored?.Invoke(this, new ErrorEventArgs(e)); + } + + public HttpClient GetHttpClient() => httpClient; + public Server GetCurrentServer() => _config.GetCurrentServer(); + public Configuration GetCurrentConfiguration() => _config; + public SystemProxyMode GetSystemProxyMode() => SystemProxy.GetCurrentMode(_config, _pacServer); + + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint) + { + if (!_config.HasConfiguredServer) + { + return Configuration.GetDefaultServer(); + } + + IStrategy strategy = GetCurrentStrategy(); + if (strategy != null) + { + return strategy.GetAServer(type, localIPEndPoint, destEndPoint); + } + return GetCurrentServer(); + } + + public void CompleteFirstRun() + { + if (!_config.firstRun) + { + return; + } + + _config.firstRun = false; + Configuration.Save(_config); + ConfigChanged?.Invoke(this, EventArgs.Empty); + } + + public void SaveServers(List servers, int localPort) + { + _config.configs = servers; + _config.localPort = localPort; + Configuration.Save(_config); + QueueDnsCryptAutomaticResolverRefresh(); + } + + public void SelectServerIndex(int index) + { + if (index == -1 && (_config.configs?.Count ?? 0) == 0) + { + _config.index = -1; + _config.strategy = null; + SaveConfig(_config); + QueueDnsCryptAutomaticResolverRefresh(); + return; + } + + if (_config.configs is null + || index < 0 + || index >= _config.configs.Count + || _config.configs[index]?.IsConfigured != true) + { + return; + } + + _config.index = index; + _config.strategy = null; + SaveConfig(_config); + QueueDnsCryptAutomaticResolverRefresh(); + } + + public bool RemoveServerAt(int index) + { + if (_config.configs is null || index < 0 || index >= _config.configs.Count) + { + return false; + } + + _config.configs.RemoveAt(index); + if (_config.configs.Count == 0) + { + _config.index = -1; + _config.strategy = string.Empty; + _config.Enabled = false; + } + else if (_config.index > index) + { + _config.index--; + } + else if (_config.index >= _config.configs.Count) + { + _config.index = _config.configs.Count - 1; + } + + SaveConfig(_config); + return true; + } + + public void ToggleShareOverLAN(bool enabled) + { + _config.shareOverLan = enabled; + SaveConfig(_config); + + ShareOverLANStatusChanged?.Invoke(this, new EventArgs()); + } + + public void SetLocalPort(int localPort) + { + Configuration.CheckLocalPort(localPort); + if (_config.localPort == localPort) + { + return; + } + + _config.localPort = localPort; + SaveConfig(_config); + } + + public void SetUiTheme(string theme) + { + string normalizedTheme = Configuration.NormalizeUiThemePreference(theme); + if (string.Equals(_config.uiTheme, normalizedTheme, StringComparison.Ordinal)) + { + return; + } + + _config.uiTheme = normalizedTheme; + Configuration.Save(_config); + ConfigChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion + + + #region Traffic routing + + public TrafficRuntimeMode GetTrafficRuntimeMode() => _gameModeManager.RuntimeMode; + + public TrafficCaptureStatus GetTrafficCaptureStatus() + { + return new TrafficCaptureStatus( + _config.trafficCaptureMode, + _gameModeManager.RuntimeMode, + _adminCaptureManager.IsBrokerRunning, + _adminCaptureManager.IsCaptureActive, + _gameModeManager.AutomaticGameMode || _adminCaptureManager.IsGameMode, + _adminCaptureManager.IsCaptureActive && _adminCaptureManager.TcpRedirectPort > 0, + _adminCaptureManager.IsCaptureActive && _adminCaptureManager.UdpRedirectPort > 0, + _adminCaptureManager.DnsInterceptionActive, + _adminCaptureManager.DnsFailClosedActive, + _adminCaptureManager.TcpRedirectPort, + _adminCaptureManager.UdpRedirectPort, + _gameModeManager.RunningGameApplications.ToArray()); + } + + public Task SetTrafficCaptureModeAsync(TrafficCaptureMode mode) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Set traffic capture mode", + cancellationToken => SetTrafficCaptureModeCoreAsync(mode, cancellationToken)); + } + + private async Task SetTrafficCaptureModeCoreAsync( + TrafficCaptureMode mode, + CancellationToken cancellationToken) + { + TrafficCaptureMode previousMode = _config.trafficCaptureMode; + _config.trafficCaptureMode = mode; + _trafficPolicyEngine.UpdateConfiguration(_config); + + try + { + await ApplyTrafficCaptureConfigurationCoreAsync(_config, cancellationToken: cancellationToken).ConfigureAwait(false); + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + Configuration.Save(_config); + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ConfigChanged?.Invoke(this, EventArgs.Empty); + return true; + } + catch (AdminElevationCanceledException exception) + { + logger.Info(exception, "Administrator elevation for Admin Mode was cancelled."); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + ReportError(exception); + } + + _config.trafficCaptureMode = previousMode == TrafficCaptureMode.Admin + ? TrafficCaptureMode.User + : previousMode; + if (_config.trafficCaptureMode == TrafficCaptureMode.Admin) + { + _config.trafficCaptureMode = TrafficCaptureMode.User; + } + Configuration.Save(_config); + try + { + await ApplyTrafficCaptureConfigurationCoreAsync(_config, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await StopDnsCryptRuntimeWhenNotRequiredAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ConfigChanged?.Invoke(this, EventArgs.Empty); + return false; + } + + public Task SaveTrafficRoutingAsync( + TrafficCaptureMode captureMode, + IEnumerable applicationRules, + IEnumerable gameModeApplications) + { + ApplicationRouteRule[] rulesSnapshot = (applicationRules ?? []).ToArray(); + string[] gamesSnapshot = (gameModeApplications ?? []).ToArray(); + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Save traffic routing", + cancellationToken => SaveTrafficRoutingCoreAsync(captureMode, rulesSnapshot, gamesSnapshot, cancellationToken)); + } + + private async Task SaveTrafficRoutingCoreAsync( + TrafficCaptureMode captureMode, + IEnumerable applicationRules, + IEnumerable gameModeApplications, + CancellationToken cancellationToken) + { + _config.applicationRules = (applicationRules ?? []) + .Where(rule => rule is not null && !string.IsNullOrWhiteSpace(rule.Application)) + .Select(rule => new ApplicationRouteRule + { + Enabled = rule.Enabled, + Application = rule.Application.Trim(), + Action = rule.Action, + }) + .ToList(); + _config.gameModeApplications = (gameModeApplications ?? []) + .Where(pattern => !string.IsNullOrWhiteSpace(pattern)) + .Select(pattern => pattern.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + _trafficPolicyEngine.UpdateConfiguration(_config); + + TrafficCaptureMode previousMode = _config.trafficCaptureMode; + _config.trafficCaptureMode = captureMode; + try + { + await ApplyTrafficCaptureConfigurationCoreAsync(_config, cancellationToken: cancellationToken).ConfigureAwait(false); + await StopDnsCryptRuntimeWhenNotRequiredAsync(cancellationToken).ConfigureAwait(false); + Configuration.Save(_config); + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ConfigChanged?.Invoke(this, EventArgs.Empty); + return true; + } + catch (AdminElevationCanceledException exception) + { + logger.Info(exception, "Administrator elevation for Admin Mode was cancelled."); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + ReportError(exception); + } + + _config.trafficCaptureMode = captureMode == TrafficCaptureMode.Admin + ? TrafficCaptureMode.User + : previousMode; + Configuration.Save(_config); + try + { + await ApplyTrafficCaptureConfigurationCoreAsync(_config, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await StopDnsCryptRuntimeWhenNotRequiredAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + } + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ConfigChanged?.Invoke(this, EventArgs.Empty); + return false; + } + + private async Task StopDnsCryptRuntimeWhenNotRequiredAsync(CancellationToken cancellationToken) + { + if (IsDnsCryptRuntimeRequired() || !_dnsCryptRuntimeManager.GetStatus().IsServing) + { + return; + } + + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + } + + private Task ApplyTrafficCaptureAsync( + Configuration configurationAtStart, + long configurationGeneration) + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Apply traffic capture configuration", + cancellationToken => ApplyTrafficCaptureStartupCoreAsync( + configurationAtStart, + configurationGeneration, + cancellationToken)); + } + + private async Task ApplyTrafficCaptureStartupCoreAsync( + Configuration configurationAtStart, + long configurationGeneration, + CancellationToken cancellationToken) + { + if (stopped + || configurationGeneration != Volatile.Read(ref _trafficConfigurationGeneration) + || !ReferenceEquals(_config, configurationAtStart)) + { + return; + } + + try + { + await ApplyTrafficCaptureConfigurationCoreAsync( + configurationAtStart, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (AdminElevationCanceledException exception) + { + logger.Info(exception, "Administrator elevation for Admin Mode was cancelled."); + if (!stopped + && configurationGeneration == Volatile.Read(ref _trafficConfigurationGeneration) + && ReferenceEquals(_config, configurationAtStart) + && _config.trafficCaptureMode == TrafficCaptureMode.Admin) + { + _config.trafficCaptureMode = TrafficCaptureMode.User; + Configuration.Save(_config); + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ConfigChanged?.Invoke(this, EventArgs.Empty); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The coordinator caller was cancelled before this configuration became active. + } + catch (Exception exception) + { + logger.LogUsefulException(exception); + if (!stopped + && configurationGeneration == Volatile.Read(ref _trafficConfigurationGeneration) + && ReferenceEquals(_config, configurationAtStart) + && _config.trafficCaptureMode == TrafficCaptureMode.Admin) + { + _config.trafficCaptureMode = TrafficCaptureMode.User; + Configuration.Save(_config); + } + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + ReportError(exception); + } + } + + private int[] GetCaptureExclusionProcessIds() + { + int dnsCryptProcessId = _dnsCryptRuntimeManager.GetStatus().ProcessId; + return _pluginsByServer.Values + .Where(plugin => plugin != null) + .Select(plugin => plugin.ProcessId) + .Where(processId => processId > 0) + .Append(Environment.ProcessId) + .Append(dnsCryptProcessId) + .Where(processId => processId > 0) + .Distinct() + .ToArray(); + } + + private DnsCaptureRuntimeState GetDnsCaptureRuntimeState() + { + DnsCryptRuntimeStatus runtime = _dnsCryptRuntimeManager.GetStatus(); + return runtime.IsServing + ? new DnsCaptureRuntimeState(runtime.Port, runtime.ProcessId) + : DnsCaptureRuntimeState.Unavailable; + } + + private async Task EnsureDnsCryptRuntimeForCaptureAsync(Configuration configuration, CancellationToken cancellationToken) + { + if (configuration?.dnsPolicy?.mode != DnsPolicyMode.DnsCrypt) + { + return; + } + + try + { + DnsCryptBootstrapPolicy.Validate(configuration, configuration.dnsPolicy?.dnsCrypt); + } + catch (InvalidOperationException exception) + { + logger.Error(exception, "DNSCrypt runtime was disabled to prevent a DNS bootstrap recursion loop."); + if (_dnsCryptRuntimeManager.GetStatus().IsServing) + { + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + } + return; + } + + if (_dnsCryptRuntimeManager.GetStatus().IsServing) + return; + + DnsCryptComponentStatus component = _dnsCryptComponentManager.GetStatus(); + if (!component.IsInstalled) + { + logger.Warn("DNSCrypt mode is selected but DNSCrypt Proxy is not installed."); + return; + } + + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + await StartDnsCryptWithResolvedResolversAsync( + configuration.dnsPolicy?.dnsCrypt, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + // DNSCrypt is intentionally fail-closed. Capture configuration must still be + // applied with an unavailable runtime so NetworkService blocks UDP/TCP 53 + // instead of silently falling back to plaintext system DNS. + logger.Warn(exception, "DNSCrypt runtime could not be prepared; DNS/53 will remain fail-closed."); + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + } + + private async Task ApplyTrafficCaptureConfigurationCoreAsync( + Configuration configuration, + bool ensureDnsRuntime = true, + CancellationToken cancellationToken = default) + { + if (configuration is null || !configuration.HasConfiguredServer) + { + // Preserve saved DNS/capture preferences, but never run DNSCrypt or WinDivert + // against an inactive relay. Adding a configured server later reapplies them. + if (_dnsCryptRuntimeManager.GetStatus().IsServing) + { + Interlocked.Increment(ref _suppressDnsCaptureRefresh); + try + { + await _dnsCryptRuntimeManager.StopAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Interlocked.Decrement(ref _suppressDnsCaptureRefresh); + } + } + + await _gameModeManager.StopAsync(cancellationToken).ConfigureAwait(false); + return; + } + + if (ensureDnsRuntime) + await EnsureDnsCryptRuntimeForCaptureAsync(configuration, cancellationToken).ConfigureAwait(false); + + await _gameModeManager.ApplyConfigurationAsync( + configuration, + GetCaptureExclusionProcessIds(), + GetDnsCaptureRuntimeState(), + cancellationToken).ConfigureAwait(false); + } + + private Task RefreshDnsCaptureAfterRuntimeChangeAsync() + { + return _dnsCryptCoordinator.ExecuteExclusiveAsync( + "Refresh DNS capture", + RefreshDnsCaptureAfterRuntimeChangeCoreAsync); + } + + private async Task RefreshDnsCaptureAfterRuntimeChangeCoreAsync(CancellationToken cancellationToken) + { + if (stopped + || !_config.HasConfiguredServer + || Volatile.Read(ref _suppressDnsCaptureRefresh) != 0 + || _config.trafficCaptureMode != TrafficCaptureMode.Admin) + { + return; + } + + await _dnsCaptureRefreshGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (stopped + || !_config.HasConfiguredServer + || Volatile.Read(ref _suppressDnsCaptureRefresh) != 0 + || _config.trafficCaptureMode != TrafficCaptureMode.Admin) + { + return; + } + + DnsCaptureRuntimeState dnsRuntime = GetDnsCaptureRuntimeState(); + if (!_adminCaptureManager.IsBrokerRunning) + { + // A game may have prevented the first Admin elevation. Keep the cached + // DNS runtime current without creating an unexpected UAC prompt; the + // latest endpoint will be used when automatic Game Mode ends. + await _gameModeManager.UpdateDnsRuntimeAsync(dnsRuntime, cancellationToken).ConfigureAwait(false); + return; + } + + await _gameModeManager.ApplyConfigurationAsync( + _config, + GetCaptureExclusionProcessIds(), + dnsRuntime, + cancellationToken).ConfigureAwait(false); + TrafficModeChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception exception) + { + logger.Warn(exception, "Unable to refresh Admin DNS capture after a DNSCrypt runtime change."); + } + finally + { + _dnsCaptureRefreshGate.Release(); + } + } + + #endregion + + #region OS Proxy + + public void ToggleEnable(bool enabled) + { + if (enabled && !_config.HasConfiguredServer) + { + logger.Warn("System proxy enable request ignored because no configured Shadowsocks server is selected."); + _config.Enabled = false; + SaveConfig(_config); + EnableStatusChanged?.Invoke(this, EventArgs.Empty); + return; + } + + _config.Enabled = enabled; + SaveConfig(_config); + + EnableStatusChanged?.Invoke(this, EventArgs.Empty); + } + + public void ToggleGlobal(bool global) + { + _config.global = global; + SaveConfig(_config); + + EnableGlobalChanged?.Invoke(this, new EventArgs()); + } + + public void SaveProxy(ForwardProxyConfig proxyConfig) + { + _config.proxy = proxyConfig; + SaveConfig(_config); + } + + private void UpdateSystemProxy() + { + SystemProxy.Update(_config, false, _pacServer, userInteraction); + } + + #endregion + + #region PAC + + private void PacDaemon_PACFileChanged(object sender, EventArgs e) + { + if (!_config.useOnlinePac) + { + RefreshManagedRoutingSnapshot("local PAC file changed"); + _pacServer.UpdatePACURL(_config); + UpdateSystemProxy(); + } + } + + private void PacServer_PACUpdateCompleted(object sender, GeositeResultEventArgs e) + { + if (!_config.useOnlinePac) + { + RefreshManagedRoutingSnapshot("GeoSite update completed"); + _pacServer.UpdatePACURL(_config); + UpdateSystemProxy(); + } + UpdatePACFromGeositeCompleted?.Invoke(this, e); + } + + private void PacServer_PACUpdateError(object sender, ErrorEventArgs e) + { + UpdatePACFromGeositeError?.Invoke(this, e); + } + + private void PacDaemon_UserRuleFileChanged(object sender, EventArgs e) + { + if (_config.useOnlinePac) + { + RefreshManagedRoutingSnapshot("user-rule.txt changed while Online PAC is active"); + return; + } + + if (GeositeUpdater.IsDatabaseAvailable) + { + GeositeUpdater.MergeAndWritePACFile(); + _pacServer.UpdatePACURL(_config); + UpdateSystemProxy(); + } + + RefreshManagedRoutingSnapshot("user-rule.txt changed"); + } + + private void RefreshManagedRoutingSnapshot(string trigger) + { + try + { + ManagedRoutingSnapshot snapshot = ManagedRoutingSnapshotBuilder.Build(_config); + _trafficPolicyEngine.UpdateManagedRouting(snapshot); + + FilterCompilationReport report = snapshot.Report; + if (report is null) + { + logger.Debug( + "Managed routing snapshot {0} disabled ({1}); trigger={2}.", + snapshot.Generation, snapshot.Source, trigger); + QueueAdminManagedRoutingRefreshIfNeeded(trigger); + return; + } + + logger.Info( + "Managed routing snapshot {0} published: mode={1}, defaultRules={2}, userRules={3}, optimizedDomains={4}, invalid={5}, source={6}; trigger={7}.", + snapshot.Generation, + snapshot.Mode, + report.DefaultRuleCount, + report.UserRuleCount, + report.OptimizedDomainRuleCount, + report.InvalidRuleCount, + snapshot.Source, + trigger); + + foreach (InvalidFilterRule invalid in report.InvalidRules.Take(5)) + { + logger.Warn("Managed routing ignored invalid filter rule '{0}': {1}", invalid.Text, invalid.Reason); + } + if (report.InvalidRuleCount > 5) + { + logger.Warn("Managed routing ignored {0} additional invalid filter rules.", report.InvalidRuleCount - 5); + } + + QueueAdminManagedRoutingRefreshIfNeeded(trigger); + } + catch (Exception exception) + { + // Atomic publication means a failed rebuild never exposes a half-compiled + // rule set. Keep the previous snapshot and retry on the next file/config event. + logger.Warn(exception, "Managed routing snapshot rebuild failed; keeping the previous snapshot. Trigger: {0}", trigger); + } + } + + private void QueueAdminManagedRoutingRefreshIfNeeded(string trigger) + { + if (_config.trafficCaptureMode != TrafficCaptureMode.Admin + || !_adminCaptureManager.IsBrokerRunning + || string.Equals(trigger, "configuration reload", StringComparison.Ordinal)) + { + return; + } + + // FileSystemWatcher may emit several notifications for one save and a second + // rule update can arrive while the capture child is already restarting. Keep a + // coalesced pending bit so the last snapshot cannot be lost behind the gate. + Volatile.Write(ref _managedRoutingCaptureRefreshTrigger, trigger ?? string.Empty); + Interlocked.Exchange(ref _managedRoutingCaptureRefreshPending, 1); + _ = RefreshAdminManagedRoutingAfterSnapshotChangeAsync(); + } + + private async Task RefreshAdminManagedRoutingAfterSnapshotChangeAsync() + { + if (!await _managedRoutingCaptureRefreshGate.WaitAsync(0).ConfigureAwait(false)) + { + return; + } + + try + { + while (Interlocked.Exchange(ref _managedRoutingCaptureRefreshPending, 0) != 0) + { + string trigger = Volatile.Read(ref _managedRoutingCaptureRefreshTrigger); + if (stopped + || _config.trafficCaptureMode != TrafficCaptureMode.Admin + || !_adminCaptureManager.IsBrokerRunning) + { + return; + } + + try + { + await _gameModeManager.ApplyConfigurationAsync( + _config, + GetCaptureExclusionProcessIds(), + GetDnsCaptureRuntimeState(), + CancellationToken.None).ConfigureAwait(false); + logger.Info("Admin managed-routing rules refreshed after snapshot change: {0}.", trigger); + } + catch (Exception exception) + { + logger.Warn(exception, "Unable to refresh Admin managed routing after snapshot change: {0}.", trigger); + } + } + } + finally + { + _managedRoutingCaptureRefreshGate.Release(); + + // Close the narrow race where a watcher marks a refresh pending after the + // loop observed zero but before this invocation released the gate. + if (Volatile.Read(ref _managedRoutingCaptureRefreshPending) != 0 + && !stopped + && _config.trafficCaptureMode == TrafficCaptureMode.Admin + && _adminCaptureManager.IsBrokerRunning) + { + _ = RefreshAdminManagedRoutingAfterSnapshotChangeAsync(); + } + } + } + + private async Task RefreshActivePacDataAsync(Configuration configAtStart) + { + try + { + // Let compound UI mode changes (e.g. PAC -> Global) finish their second + // config write before deciding whether a PAC download is still required. + await Task.Yield(); + if (!ReferenceEquals(_config, configAtStart)) + return; + + // PAC data is only fetched when PAC mode is actually active. Global and + // Disabled modes do not need either GeoSite or an online PAC refresh. + if (!configAtStart.Enabled || configAtStart.global) + return; + + if (configAtStart.useOnlinePac) + { + if (string.IsNullOrWhiteSpace(configAtStart.pacUrl)) + return; + + await OnlinePacCache.RefreshAsync(configAtStart); + + // A reload may have changed the mode or URL while the request was in flight. + if (_config.Enabled && !_config.global && _config.useOnlinePac && + string.Equals(_config.pacUrl, configAtStart.pacUrl, StringComparison.Ordinal)) + { + _pacServer.UpdatePACURL(_config); + UpdateSystemProxy(); + } + return; + } + + if (!GeositeUpdater.IsDatabaseAvailable || GeositeUpdater.NeedsRefresh) + { + await GeositeUpdater.UpdatePACFromGeosite(_config, false); + if (_config.Enabled && !_config.global && !_config.useOnlinePac) + { + _pacServer.UpdatePACURL(_config); + UpdateSystemProxy(); + } + } + } + catch (Exception ex) + { + // Keep the last known-good cache (or the proxy-all bootstrap PAC) and + // retry on the next relevant reload/start instead of breaking the client. + logger.LogUsefulException(ex); + } + } + + public string GetPacUrl() => _pacServer.PacUrl; + + public ManagedRoutingStatus GetManagedRoutingStatus() + { + ManagedRoutingSnapshot snapshot = _trafficPolicyEngine.CurrentManagedRoutingSnapshot; + FilterCompilationReport report = snapshot.Report; + string effectiveMode = _config.useOnlinePac + ? "Online PAC" + : "Managed routing"; + + return new ManagedRoutingStatus( + effectiveMode, + snapshot.Mode.ToString(), + snapshot.Generation, + snapshot.IsEnabled, + snapshot.Source, + snapshot.CreatedUtc, + report?.DefaultRuleCount ?? 0, + report?.UserRuleCount ?? 0, + report?.InvalidRuleCount ?? 0, + snapshot.DirectDecisionCount, + snapshot.ProxyDecisionCount, + _adminCaptureManager.ManagedRoutingActive, + _adminCaptureManager.ManagedRoutingRuleCount); + } + + public void CopyPacUrl() + { + userInteraction.SetClipboardText(_pacServer.PacUrl); + } + + public void SavePACUrl(string pacUrl) + { + _config.pacUrl = pacUrl; + SaveConfig(_config); + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + public void SaveGeositeSources(List sources) + { + _config.geositeUrls = Configuration.NormalizeGeositeSourceList(sources); + + // The existing pac.txt may have been generated from a different set of + // databases. Remove it before reload so Local PAC is rebuilt from the new + // merged source set (or from the safe proxy-all bootstrap while downloading). + if (!_config.useOnlinePac) + { + try + { + File.Delete(PACDaemon.PacFile); + } + catch (Exception ex) + { + logger.LogUsefulException(ex); + } + } + + SaveConfig(_config); + } + + public void UseOnlinePAC(bool useOnlinePac) + { + if (useOnlinePac && !_config.HasConfiguredServer) + { + logger.Warn("Online PAC enable request ignored because no configured Shadowsocks server is selected."); + return; + } + + _config.useOnlinePac = useOnlinePac; + SaveConfig(_config); + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + public Task UpdatePACFromGeositeAsync() + { + return GeositeUpdater.UpdatePACFromGeosite(_config); + } + + public void TouchUserRuleFile() + { + string userRuleFilename = PACDaemon.TouchUserRuleFile(); + + UserRuleFileReadyToOpen?.Invoke(this, new PathEventArgs() { Path = userRuleFilename }); + } + + public void ToggleSecureLocalPac(bool enabled) + { + _config.secureLocalPac = enabled; + SaveConfig(_config); + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + public void ToggleRegeneratePacOnUpdate(bool enabled) + { + _config.regeneratePacOnUpdate = enabled; + SaveConfig(_config); + ConfigChanged?.Invoke(this, new EventArgs()); + } + + #endregion + + #region SIP002 + + public bool AskAddServerBySSURL(string ssURL) + { + if (userInteraction.Confirm(I18N.GetString("Import from URL: {0} ?", ssURL), I18N.GetString("shadowsocks-reborn"))) + { + if (AddServerBySSURL(ssURL)) + { + userInteraction.ShowInfo(I18N.GetString("Successfully imported from {0}", ssURL), I18N.GetString("shadowsocks-reborn")); + return true; + } + else + { + userInteraction.ShowError(I18N.GetString("Failed to import. Please check if the link is valid."), I18N.GetString("shadowsocks-reborn")); + } + } + return false; + } + + public bool AddServerBySSURL(string ssURL) + { + try + { + if (string.IsNullOrWhiteSpace(ssURL)) + return false; + + var servers = Server.GetServers(ssURL); + if (servers == null || servers.Count == 0) + return false; + + foreach (var server in servers) + { + _config.configs.Add(server); + } + _config.index = _config.configs.Count - 1; + SaveConfig(_config); + ConfigChanged?.Invoke(this, EventArgs.Empty); + return true; + } + catch (Exception e) + { + logger.LogUsefulException(e); + return false; + } + } + + public string GetServerURLForCurrentServer() + { + return GetCurrentServer().GetURL(); + } + + #endregion + + #region Misc + + public void ToggleVerboseLogging(bool enabled) + { + if (_config.isVerboseLogging == enabled) + return; + + _config.isVerboseLogging = enabled; + // Log level changes do not require a network/controller reload. Reconfigure NLog + // in place so toggling a logging option never restarts listeners or SIP003 plugins. + Configuration.Save(_config); + LoggingConfigurator.Configure(enabled); + + VerboseLoggingStatusChanged?.Invoke(this, EventArgs.Empty); + } + + public void ToggleCheckingUpdate(bool enabled) + { + _config.autoCheckUpdate = enabled; + Configuration.Save(_config); + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + public void ToggleCheckingPreRelease(bool enabled) + { + _config.checkPreRelease = enabled; + Configuration.Save(_config); + ConfigChanged?.Invoke(this, new EventArgs()); + } + + public void SaveSkippedUpdateVerion(string version) + { + _config.skippedUpdateVersion = version; + Configuration.Save(_config); + } + + public void SaveLogViewerConfig(LogViewerConfig newConfig) + { + _config.logViewer = newConfig; + // Viewer-only preferences do not affect proxy/runtime state. Broadcasting a global + // configuration event here caused MainWindow to refresh and rebuild the Logs page. + Configuration.Save(_config); + } + + public void SaveHotkeyConfig(HotkeyConfig newConfig) + { + _config.hotkey = newConfig; + SaveConfig(_config); + + ConfigChanged?.Invoke(this, new EventArgs()); + } + + #endregion + + #region Strategy + + public void SelectStrategy(string strategyID) + { + _config.index = -1; + _config.strategy = strategyID; + SaveConfig(_config); + QueueDnsCryptAutomaticResolverRefresh(); + } + + public IList GetStrategies() + { + return _strategyManager.GetStrategies(); + } + + public IStrategy GetCurrentStrategy() + { + foreach (var strategy in _strategyManager.GetStrategies()) + { + if (strategy.ID == _config.strategy) + { + return strategy; + } + } + return null; + } + + public void UpdateInboundCounter(object sender, SSTransmitEventArgs args) + { + GetCurrentStrategy()?.UpdateLastRead(args.Server); + Interlocked.Add(ref _inboundCounter, args.Length); + } + + public void UpdateOutboundCounter(object sender, SSTransmitEventArgs args) + { + GetCurrentStrategy()?.UpdateLastWrite(args.Server); + Interlocked.Add(ref _outboundCounter, args.Length); + } + + #endregion + + #region SIP003 + + private void StartPlugin() + { + var server = _config.GetCurrentServer(); + if (server?.IsConfigured != true || string.IsNullOrWhiteSpace(server.plugin)) + { + return; + } + + GetPluginLocalEndPointIfConfigured(server); + } + + private void StopPlugins() + { + foreach (var serverAndPlugin in _pluginsByServer) + { + serverAndPlugin.Value?.Dispose(); + } + _pluginsByServer.Clear(); + } + + public EndPoint GetPluginLocalEndPointIfConfigured(Server server) + { + if (server?.IsConfigured != true || string.IsNullOrWhiteSpace(server.plugin)) + { + return null; + } + + var plugin = _pluginsByServer.GetOrAdd( + server, + x => Sip003Plugin.CreateIfConfigured(x, () => _config.showPluginOutput)); + + if (plugin == null) + { + return null; + } + + try + { + if (plugin.StartIfNeeded()) + { + logger.Info( + $"Started SIP003 plugin for {server.Identifier()} on {plugin.LocalEndPoint} - PID: {plugin.ProcessId}"); + } + } + catch (Exception ex) + { + logger.Error("Failed to start SIP003 plugin: " + ex.Message); + throw; + } + + return plugin.LocalEndPoint; + } + + public void ToggleShowPluginOutput(bool enabled) + { + if (_config.showPluginOutput == enabled) + return; + + _config.showPluginOutput = enabled; + // Logging visibility is a presentation/runtime flag. A full controller reload restarts + // the listener and SIP003 process, which is unnecessary and can block plugin shutdown. + Configuration.Save(_config); + ShowPluginOutputChanged?.Invoke(this, EventArgs.Empty); + } + + public void ToggleShowDnsLogs(bool enabled) + { + if (_config.showDnsLogs == enabled) + return; + + _config.showDnsLogs = enabled; + Configuration.Save(_config); + + ShowDnsLogsChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion + + #region Traffic Statistics + + public IReadOnlyList GetTrafficSnapshot(int maxSamples) + { + if (maxSamples <= 0) + return Array.Empty(); + + lock (_trafficStatisticsSync) + { + if (_trafficPerSecondQueue is null || _trafficPerSecondQueue.Count == 0) + return Array.Empty(); + + return _trafficPerSecondQueue.TakeLast(maxSamples).ToArray(); + } + } + + public TrafficPerSecond GetLatestTrafficSample() + { + lock (_trafficStatisticsSync) + return _trafficPerSecondQueue?.LastOrDefault(); + } + + private void StartTrafficStatistics(int queueMaxSize) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(queueMaxSize); + + lock (_trafficStatisticsSync) + { + _trafficPerSecondQueue = new Queue(queueMaxSize); + for (int i = 0; i < queueMaxSize; i++) + _trafficPerSecondQueue.Enqueue(new TrafficPerSecond()); + } + + _trafficStatisticsCancellation = new CancellationTokenSource(); + _trafficStatisticsTask = Task.Run(() => TrafficStatisticsAsync(queueMaxSize, _trafficStatisticsCancellation.Token)); + } + + private void StopTrafficStatistics() + { + CancellationTokenSource cancellation = _trafficStatisticsCancellation; + Task task = _trafficStatisticsTask; + _trafficStatisticsCancellation = null; + _trafficStatisticsTask = null; + + if (cancellation is null) + return; + + try + { + cancellation.Cancel(); + task?.GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + // Normal controller disposal. + } + catch (Exception exception) + { + logger.Warn(exception, "Traffic statistics task did not stop cleanly."); + } + finally + { + cancellation.Dispose(); + } + } + + private async Task TrafficStatisticsAsync(int queueMaxSize, CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_trafficStatisticsSync) + { + TrafficPerSecond previous = _trafficPerSecondQueue.Last(); + long inboundCounter = InboundCounter; + long outboundCounter = OutboundCounter; + var current = new TrafficPerSecond + { + InboundCounter = inboundCounter, + OutboundCounter = outboundCounter, + InboundIncrement = inboundCounter - previous.InboundCounter, + OutboundIncrement = outboundCounter - previous.OutboundCounter, + }; + + _trafficPerSecondQueue.Enqueue(current); + if (_trafficPerSecondQueue.Count > queueMaxSize) + _trafficPerSecondQueue.Dequeue(); + } + + TrafficChanged?.Invoke(this, EventArgs.Empty); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Normal controller disposal. + } + } + + #endregion + + #region SIP008 + + + public async Task UpdateOnlineConfigInternal(string url) + { + var onlineServer = await OnlineConfigResolver.GetOnline(httpClient, url); + _config.configs = Configuration.SortByOnlineConfig( + _config.configs + .Where(c => c.group != url) + .Concat(onlineServer) + ); + logger.Info($"updated {onlineServer.Count} server from {url}"); + return onlineServer.Count; + } + + public async Task UpdateOnlineConfig(string url) + { + var selected = GetCurrentServer(); + try + { + int count = await UpdateOnlineConfigInternal(url); + } + catch (Exception e) + { + logger.LogUsefulException(e); + return false; + } + _config.index = FindServerIndexAfterOnlineRefresh(_config.configs, selected); + SaveConfig(_config); + return true; + } + + public async Task> UpdateAllOnlineConfig() + { + var selected = GetCurrentServer(); + var failedUrls = new List(); + foreach (var url in _config.onlineConfigSource) + { + try + { + await UpdateOnlineConfigInternal(url); + } + catch (Exception e) + { + logger.LogUsefulException(e); + failedUrls.Add(url); + } + } + + _config.index = FindServerIndexAfterOnlineRefresh(_config.configs, selected); + SaveConfig(_config); + return failedUrls; + } + + private static int FindServerIndexAfterOnlineRefresh(List servers, Server selected) + { + if (servers is null || servers.Count == 0 || selected is null) + { + return -1; + } + + for (int index = 0; index < servers.Count; index++) + { + if (ReferenceEquals(servers[index], selected)) + { + return index; + } + } + + for (int index = 0; index < servers.Count; index++) + { + Server candidate = servers[index]; + if (candidate is null) + { + continue; + } + + if (string.Equals(candidate.group, selected.group, StringComparison.Ordinal) + && string.Equals(candidate.server, selected.server, StringComparison.OrdinalIgnoreCase) + && candidate.ServerPort == selected.ServerPort) + { + return index; + } + } + + return -1; + } + + public void SaveOnlineConfigSource(List sources) + { + _config.onlineConfigSource = sources; + SaveConfig(_config); + } + + public void RemoveOnlineConfig(string url) + { + _config.onlineConfigSource.RemoveAll(v => v == url); + _config.configs = Configuration.SortByOnlineConfig( + _config.configs.Where(c => c.group != url) + ); + SaveConfig(_config); + } + + #endregion + } +} diff --git a/Shadowsocks.Windows/Controller/System/AutoStartup.cs b/Shadowsocks.Windows/Controller/System/AutoStartup.cs new file mode 100644 index 000000000..cb6e144e3 --- /dev/null +++ b/Shadowsocks.Windows/Controller/System/AutoStartup.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.Win32; +using NLog; +using Shadowsocks.Core; +using AppRuntimeEnvironment = Shadowsocks.Core.RuntimeEnvironment; +using Shadowsocks.Core.Storage; +using Shadowsocks.Util; +using Shadowsocks.Windows.Shell; + +namespace Shadowsocks.Controller +{ + public static partial class AutoStartup + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string Key = "Shadowsocks Reborn"; + internal const string StartupHiddenOption = "--start-hidden"; + internal const string StartupVisibleOption = "--start-visible"; + internal const string StartupOriginOption = "--startup-origin"; + private static bool StartupCopySynchronized; + private static int TrackedWindowVisibility = -1; + + public static bool Set(bool enabled, bool windowVisible = false) + { + if (AppStoragePaths.IsCleanMode) + { + Logger.Info("Start with Windows is unavailable in Clean Mode."); + return false; + } + + RegistryKey runKey = null; + try + { + runKey = WindowsSystemUtilities.OpenRegistryKey(RunKeyPath, true); + if (runKey == null) + { + Logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); + return false; + } + + if (enabled) + { + if (!EnsureStartupExecutable()) + { + return false; + } + + runKey.SetValue(Key, BuildStartupCommand(windowVisible)); + RemoveDuplicateStartupEntries(runKey); + } + else + { + runKey.DeleteValue(Key, throwOnMissingValue: false); + // Disabling Start with Windows must also remove legacy Shadowsocks + // Run values. Otherwise an obsolete key can continue launching a + // second process even though the current UI reports autostart off. + RemoveDuplicateStartupEntries(runKey); + TryDeleteUnusedStartupCopy(); + } + + // When autostartup setting changes, change RegisterForRestart state to avoid starting twice. + RegisterForRestart(!enabled, windowVisible); + return true; + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + return false; + } + finally + { + runKey?.Dispose(); + } + } + + public static bool Check() + { + if (AppStoragePaths.IsCleanMode) + { + return false; + } + + RegistryKey runKey = null; + try + { + runKey = WindowsSystemUtilities.OpenRegistryKey(RunKeyPath, true); + if (runKey == null) + { + Logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); + return false; + } + + string command = runKey.GetValue(Key)?.ToString(); + if (!IsStartupExecutableCommand(command)) + { + string primaryExecutable = GetPrimaryExecutablePath(); + string currentExecutable = AppRuntimeEnvironment.ExecutablePath; + bool canonicalTargetsProduct = CommandTargetsExecutable(command, primaryExecutable) + || CommandTargetsExecutable(command, currentExecutable); + bool legacyTargetsProduct = HasLegacyStartupEntry( + runKey, + AppStoragePaths.StartupExecutableFile, + primaryExecutable, + currentExecutable); + if (!canonicalTargetsProduct && !legacyTargetsProduct) + { + return false; + } + + if (!EnsureStartupExecutable()) + { + return false; + } + + command = BuildStartupCommand(windowVisible: false); + runKey.SetValue(Key, command); + Logger.Info("Migrated legacy Start with Windows command to the stable LocalAppData startup executable."); + } + + // A user may update Shadowsocks from a USB drive/download folder while autostart + // is already enabled. Keep the stable LocalAppData copy synchronized with the + // currently running product binary without changing the Run key location. + if (!StartupCopySynchronized + && !PathsEqual(AppRuntimeEnvironment.ExecutablePath, AppStoragePaths.StartupExecutableFile)) + { + if (!EnsureStartupExecutable()) + { + Logger.Warn("Start with Windows is configured, but the LocalAppData startup copy could not be refreshed."); + } + } + + string[] commandArguments = WindowsCommandLine.ParseArguments(command ?? string.Empty); + bool windowVisible = !IsHiddenStartup(commandArguments) && IsVisibleStartup(commandArguments); + string canonical = BuildStartupCommand(windowVisible); + if (!string.Equals(command?.Trim(), canonical, StringComparison.OrdinalIgnoreCase)) + { + runKey.SetValue(Key, canonical); + } + RemoveDuplicateStartupEntries(runKey); + return true; + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + return false; + } + finally + { + runKey?.Dispose(); + } + } + + private static bool EnsureStartupExecutable() + { + if (AppStoragePaths.IsCleanMode) + { + return false; + } + + string source = GetPrimaryExecutablePath(); + string destination = AppStoragePaths.StartupExecutableFile; + try + { + if (PathsEqual(source, destination)) + { + StartupCopySynchronized = File.Exists(destination); + return StartupCopySynchronized; + } + if (!File.Exists(source)) + { + Logger.Error("Cannot create the autostart copy because the current executable does not exist: {0}", source); + return false; + } + + Directory.CreateDirectory(AppStoragePaths.StartupDirectory); + if (File.Exists(destination) && FilesMatch(source, destination)) + { + StartupCopySynchronized = true; + return true; + } + + string staging = Path.Combine( + AppStoragePaths.StartupDirectory, + $"Shadowsocks.exe.new.{Environment.ProcessId}.{Guid.NewGuid():N}"); + try + { + File.Copy(source, staging, overwrite: true); + if (!FilesMatch(source, staging)) + { + throw new IOException("The copied autostart executable failed SHA-256 validation."); + } + + File.Move(staging, destination, overwrite: true); + if (!FilesMatch(source, destination)) + { + throw new IOException("The installed autostart executable failed SHA-256 validation."); + } + } + finally + { + TryDeleteFile(staging); + } + + StartupCopySynchronized = true; + Logger.Info("Installed Start with Windows executable at {0}", destination); + return true; + } + catch (Exception exception) + { + Logger.Error(exception, "Unable to install the Start with Windows executable in LocalAppData."); + return false; + } + } + + private static bool FilesMatch(string first, string second) + { + var firstInfo = new FileInfo(first); + var secondInfo = new FileInfo(second); + if (!firstInfo.Exists || !secondInfo.Exists || firstInfo.Length != secondInfo.Length) + { + return false; + } + + using FileStream firstStream = File.OpenRead(first); + using FileStream secondStream = File.OpenRead(second); + byte[] firstHash = SHA256.HashData(firstStream); + byte[] secondHash = SHA256.HashData(secondStream); + return CryptographicOperations.FixedTimeEquals(firstHash, secondHash); + } + + private static string BuildStartupCommand(bool windowVisible) + { + string executable = $"\"{AppStoragePaths.StartupExecutableFile}\""; + string uiStateArgument = windowVisible ? StartupVisibleOption : StartupHiddenOption; + string primaryExecutable = GetPrimaryExecutablePath(); + return $"{executable} {uiStateArgument} {StartupOriginOption} \"{primaryExecutable}\""; + } + + /// + /// Returns the user-facing product executable that should be updated. When Windows + /// starts the stable LocalAppData startup copy, the original product path is carried + /// in the Run command so an automatic update cannot update only the startup copy. + /// + internal static string GetPrimaryExecutablePath() + => ResolvePrimaryExecutablePath( + AppRuntimeEnvironment.ExecutablePath, + AppStoragePaths.StartupExecutableFile, + AppRuntimeEnvironment.Arguments); + + internal static string ResolvePrimaryExecutablePath( + string currentExecutablePath, + string startupExecutablePath, + IReadOnlyList arguments) + { + string current = Path.GetFullPath(currentExecutablePath); + string startup = Path.GetFullPath(startupExecutablePath); + if (!PathsEqual(current, startup)) + { + return current; + } + + string origin = GetOption(arguments, StartupOriginOption); + if (string.IsNullOrWhiteSpace(origin)) + { + return current; + } + + try + { + string candidate = Path.GetFullPath(origin); + string fileName = Path.GetFileNameWithoutExtension(candidate); + if (File.Exists(candidate) + && fileName.StartsWith("Shadowsocks", StringComparison.OrdinalIgnoreCase) + && !PathsEqual(candidate, startup)) + { + return candidate; + } + } + catch + { + } + + return current; + } + + private static string GetOption(IReadOnlyList arguments, string option) + { + if (arguments == null) + { + return null; + } + + for (int index = 0; index < arguments.Count; index++) + { + string argument = arguments[index] ?? string.Empty; + if (argument.StartsWith(option + "=", StringComparison.OrdinalIgnoreCase)) + { + return argument.Substring(option.Length + 1).Trim('\"'); + } + if (argument.Equals(option, StringComparison.OrdinalIgnoreCase) && index + 1 < arguments.Count) + { + return (arguments[index + 1] ?? string.Empty).Trim('\"'); + } + } + return null; + } + + private static bool HasLegacyStartupEntry(RegistryKey runKey, params string[] executablePaths) + { + foreach (string valueName in runKey.GetValueNames()) + { + if (valueName.Equals(Key, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string command = runKey.GetValue(valueName)?.ToString(); + if (executablePaths.Any(path => CommandTargetsExecutable(command, path))) + { + return true; + } + } + + return false; + } + + private static void RemoveDuplicateStartupEntries(RegistryKey runKey) + { + string startupExecutable = AppStoragePaths.StartupExecutableFile; + string primaryExecutable = GetPrimaryExecutablePath(); + string currentExecutable = AppRuntimeEnvironment.ExecutablePath; + + foreach (string valueName in runKey.GetValueNames()) + { + if (valueName.Equals(Key, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string command = runKey.GetValue(valueName)?.ToString(); + if (CommandTargetsExecutable(command, startupExecutable) + || CommandTargetsExecutable(command, primaryExecutable) + || CommandTargetsExecutable(command, currentExecutable)) + { + runKey.DeleteValue(valueName, throwOnMissingValue: false); + Logger.Info("Removed duplicate Start with Windows registry entry '{0}'.", valueName); + } + } + } + + internal static bool CommandTargetsExecutable(string command, string executablePath) + { + if (string.IsNullOrWhiteSpace(command) || string.IsNullOrWhiteSpace(executablePath)) + { + return false; + } + + string target; + try + { + target = Path.GetFullPath(executablePath); + } + catch + { + return false; + } + + string[] parsed = WindowsCommandLine.ParseArguments(command); + if (parsed.Length > 0 && PathsEqual(parsed[0], target)) + { + return true; + } + + // Older Shadowsocks versions could persist an unquoted executable path. + // Preserve compatibility long enough to remove that stale Run entry. + string trimmed = command.Trim(); + string quotedTarget = $"\"{target}\""; + return trimmed.Equals(target, StringComparison.OrdinalIgnoreCase) + || trimmed.Equals(quotedTarget, StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith(quotedTarget + " ", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith(target + " ", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsStartupExecutableCommand(string command) + { + if (string.IsNullOrWhiteSpace(command)) + { + return false; + } + + string path = AppStoragePaths.StartupExecutableFile; + string trimmed = command.Trim(); + string quoted = $"\"{path}\""; + return trimmed.Equals(path, StringComparison.OrdinalIgnoreCase) + || trimmed.Equals(quoted, StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith(quoted + " ", StringComparison.OrdinalIgnoreCase); + } + + private static void TryDeleteUnusedStartupCopy() + { + if (PathsEqual(AppRuntimeEnvironment.ExecutablePath, AppStoragePaths.StartupExecutableFile)) + { + return; + } + TryDeleteFile(AppStoragePaths.StartupExecutableFile); + StartupCopySynchronized = false; + } + + private static void TryDeleteFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to remove autostart staging/copy file {0}", path); + } + } + + private static bool PathsEqual(string first, string second) + { + if (string.IsNullOrWhiteSpace(first) || string.IsNullOrWhiteSpace(second)) + { + return false; + } + return string.Equals( + Path.GetFullPath(first).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + Path.GetFullPath(second).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase); + } + + [LibraryImport("kernel32.dll", EntryPoint = "RegisterApplicationRestart", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + private static partial int RegisterApplicationRestart(string commandLineArgs, int flags); + + [LibraryImport("kernel32.dll", EntryPoint = "UnregisterApplicationRestart", SetLastError = true)] + private static partial int UnregisterApplicationRestart(); + + [Flags] + private enum ApplicationRestartFlags + { + RestartAlways = 0, + RestartNoCrash = 1, + RestartNoHang = 2, + RestartNoPatch = 4, + RestartNoReboot = 8, + } + + internal static bool IsHiddenStartup(IReadOnlyList arguments) + => HasStartupOption(arguments, StartupHiddenOption); + + internal static bool IsVisibleStartup(IReadOnlyList arguments) + => HasStartupOption(arguments, StartupVisibleOption); + + private static bool HasStartupOption(IReadOnlyList arguments, string option) + => arguments?.Any(argument => + string.Equals(argument, option, StringComparison.OrdinalIgnoreCase)) == true; + + internal static string BuildRestartCommandLine(IReadOnlyList arguments, bool windowVisible) + { + var restartArguments = new List(); + foreach (string argument in arguments ?? Array.Empty()) + { + if (string.Equals(argument, StartupHiddenOption, StringComparison.OrdinalIgnoreCase) + || string.Equals(argument, StartupVisibleOption, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + restartArguments.Add(argument); + } + + restartArguments.Add(windowVisible ? StartupVisibleOption : StartupHiddenOption); + + return string.Join( + " ", + restartArguments + .Select(argument => argument.Replace("\"", "\\\"")) + .Select(argument => argument.Contains(' ') ? "\"" + argument + "\"" : argument)); + } + + /// + /// Keeps Windows reboot/session restoration synchronized with the shell's + /// actual visibility. Start with Windows owns restoration when enabled; + /// otherwise Restart Manager owns it. The mechanisms remain mutually exclusive. + /// + public static void SynchronizeUiState(bool windowVisible) + { + System.Threading.Volatile.Write(ref TrackedWindowVisibility, windowVisible ? 1 : 0); + if (AppStoragePaths.IsCleanMode) + { + return; + } + + RegistryKey runKey = null; + try + { + bool startupEnabled = Check(); + if (!startupEnabled) + { + RegisterForRestart(true, windowVisible); + return; + } + + runKey = WindowsSystemUtilities.OpenRegistryKey(RunKeyPath, true); + if (runKey == null) + { + Logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); + RegisterForRestart(true, windowVisible); + return; + } + + runKey.SetValue(Key, BuildStartupCommand(windowVisible)); + RemoveDuplicateStartupEntries(runKey); + RegisterForRestart(false, windowVisible); + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + } + finally + { + runKey?.Dispose(); + } + } + + internal static bool TryGetTrackedWindowVisibility(out bool windowVisible) + { + int tracked = System.Threading.Volatile.Read(ref TrackedWindowVisibility); + windowVisible = tracked == 1; + return tracked >= 0; + } + + public static void RegisterForRestart(bool register, bool windowVisible = false) + { + if (AppStoragePaths.IsCleanMode) + { + if (!register) + { + UnregisterApplicationRestart(); + } + return; + } + + if (!register) + { + UnregisterApplicationRestart(); + Logger.Debug("Unregister restart after system reboot"); + return; + } + + string cmdline = BuildRestartCommandLine(AppRuntimeEnvironment.Arguments, windowVisible); + int result = RegisterApplicationRestart( + cmdline, + (int)(ApplicationRestartFlags.RestartNoCrash | ApplicationRestartFlags.RestartNoHang)); + if (result != 0) + { + Logger.Warn("RegisterApplicationRestart failed with HRESULT 0x{0:X8}.", result); + return; + } + + Logger.Debug("Register restart after system reboot, command line: " + cmdline); + } + } +} diff --git a/Shadowsocks.Windows/Controller/System/Hotkeys/GlobalHotkeyService.cs b/Shadowsocks.Windows/Controller/System/Hotkeys/GlobalHotkeyService.cs new file mode 100644 index 000000000..bc56a7e8d --- /dev/null +++ b/Shadowsocks.Windows/Controller/System/Hotkeys/GlobalHotkeyService.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Shadowsocks.Controller.Hotkeys +{ + public readonly record struct HotkeyGesture(uint Modifiers, uint VirtualKey, string KeyName) + { + private const uint ModAlt = 0x0001; + private const uint ModControl = 0x0002; + private const uint ModShift = 0x0004; + private const uint ModWin = 0x0008; + private const uint ModNoRepeat = 0x4000; + + public static bool TryParse(string value, out HotkeyGesture gesture) + { + gesture = default; + if (string.IsNullOrWhiteSpace(value)) return false; + + string[] parts = value.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length < 2) return false; + + uint modifiers = ModNoRepeat; + for (int i = 0; i < parts.Length - 1; i++) + { + switch (parts[i].ToUpperInvariant()) + { + case "CTRL": + case "CONTROL": modifiers |= ModControl; break; + case "ALT": modifiers |= ModAlt; break; + case "SHIFT": modifiers |= ModShift; break; + case "WIN": + case "WINDOWS": modifiers |= ModWin; break; + default: return false; + } + } + + string keyName = parts[^1]; + if (!TryGetVirtualKey(keyName, out uint virtualKey)) return false; + gesture = new HotkeyGesture(modifiers, virtualKey, NormalizeKeyName(keyName)); + return true; + } + + public override string ToString() + { + List parts = new(); + if ((Modifiers & ModControl) != 0) parts.Add("Ctrl"); + if ((Modifiers & ModAlt) != 0) parts.Add("Alt"); + if ((Modifiers & ModShift) != 0) parts.Add("Shift"); + if ((Modifiers & ModWin) != 0) parts.Add("Win"); + parts.Add(KeyName); + return string.Join("+", parts); + } + + private static string NormalizeKeyName(string keyName) + { + if (keyName.Length == 1) return keyName.ToUpperInvariant(); + if (keyName.StartsWith("d", StringComparison.OrdinalIgnoreCase) && keyName.Length == 2 && char.IsDigit(keyName[1])) + return "D" + keyName[1]; + if (keyName.StartsWith("numpad", StringComparison.OrdinalIgnoreCase)) + return "NumPad" + keyName[6..]; + if (keyName.StartsWith("f", StringComparison.OrdinalIgnoreCase) && int.TryParse(keyName[1..], out _)) + return "F" + keyName[1..]; + return keyName; + } + + private static bool TryGetVirtualKey(string keyName, out uint key) + { + key = 0; + if (keyName.Length == 1) + { + char c = char.ToUpperInvariant(keyName[0]); + if (c is >= 'A' and <= 'Z' or >= '0' and <= '9') { key = c; return true; } + } + + if (keyName.Length == 2 && (keyName[0] == 'D' || keyName[0] == 'd') && char.IsDigit(keyName[1])) + { key = keyName[1]; return true; } + + if (keyName.StartsWith("NumPad", StringComparison.OrdinalIgnoreCase) && keyName.Length == 7 && char.IsDigit(keyName[6])) + { key = 0x60u + (uint)(keyName[6] - '0'); return true; } + + if (keyName.StartsWith("F", StringComparison.OrdinalIgnoreCase) && int.TryParse(keyName[1..], out int functionKey) && functionKey is >= 1 and <= 24) + { key = 0x70u + (uint)(functionKey - 1); return true; } + + return NamedKeys.TryGetValue(keyName, out key); + } + + private static readonly Dictionary NamedKeys = new(StringComparer.OrdinalIgnoreCase) + { + ["Back"] = 0x08, ["Tab"] = 0x09, ["Enter"] = 0x0D, ["Return"] = 0x0D, + ["Escape"] = 0x1B, ["Space"] = 0x20, ["PageUp"] = 0x21, ["PageDown"] = 0x22, + ["End"] = 0x23, ["Home"] = 0x24, ["Left"] = 0x25, ["Up"] = 0x26, + ["Right"] = 0x27, ["Down"] = 0x28, ["Insert"] = 0x2D, ["Delete"] = 0x2E, + ["Oem1"] = 0xBA, ["OemPlus"] = 0xBB, ["OemComma"] = 0xBC, ["OemMinus"] = 0xBD, + ["OemPeriod"] = 0xBE, ["Oem2"] = 0xBF, ["Oem3"] = 0xC0, ["Oem4"] = 0xDB, + ["Oem5"] = 0xDC, ["Oem6"] = 0xDD, ["Oem7"] = 0xDE, + }; + } + + public sealed partial class GlobalHotkeyService : IDisposable + { + private const uint WmHotkey = 0x0312; + private const uint WmAppRegister = 0x8001; + private const uint WmAppUnregister = 0x8002; + private const uint WmAppStop = 0x8003; + private const uint PmNoRemove = 0x0000; + + private readonly Thread _thread; + private readonly ManualResetEventSlim _ready = new(false); + private readonly ConcurrentDictionary _commands = new(); + private readonly Dictionary _byName = new(StringComparer.Ordinal); + private readonly Dictionary _byId = new(); + private int _nextCommandId; + private int _nextHotkeyId = 100; + private uint _threadId; + private bool _disposed; + + public GlobalHotkeyService() + { + _thread = new Thread(MessageLoop) { IsBackground = true, Name = "Shadowsocks.GlobalHotkeys" }; + _thread.Start(); + _ready.Wait(); + } + + public bool Register(string name, string gestureText, Action callback) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(callback); + if (!HotkeyGesture.TryParse(gestureText, out HotkeyGesture gesture)) return false; + + Unregister(name); + int hotkeyId = Interlocked.Increment(ref _nextHotkeyId); + var registration = new Registration(name, hotkeyId, gesture, callback); + var command = new Command(registration); + return Execute(WmAppRegister, command); + } + + public void Unregister(string name) + { + if (_disposed || string.IsNullOrWhiteSpace(name)) return; + Execute(WmAppUnregister, new Command(name)); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _ = PostThreadMessage(_threadId, WmAppStop, UIntPtr.Zero, IntPtr.Zero); + _thread.Join(TimeSpan.FromSeconds(2)); + _ready.Dispose(); + } + + private bool Execute(uint message, Command command) + { + int commandId = Interlocked.Increment(ref _nextCommandId); + _commands[commandId] = command; + if (PostThreadMessage(_threadId, message, (UIntPtr)(uint)commandId, IntPtr.Zero) == 0) + { + _commands.TryRemove(commandId, out _); + return false; + } + command.Completed.Wait(); + command.Completed.Dispose(); + return command.Result; + } + + private void MessageLoop() + { + _threadId = GetCurrentThreadId(); + PeekMessage(out _, IntPtr.Zero, 0, 0, PmNoRemove); + _ready.Set(); + + while (GetMessage(out NativeMessage message, IntPtr.Zero, 0, 0) > 0) + { + if (message.MessageId == WmHotkey) + { + int id = unchecked((int)message.WParam.ToUInt64()); + if (_byId.TryGetValue(id, out Registration registration)) + { + try { registration.Callback(); } catch { } + } + continue; + } + + if (message.MessageId == WmAppStop) + { + foreach (Registration registration in _byId.Values) + _ = UnregisterHotKey(IntPtr.Zero, registration.Id); + _byId.Clear(); + _byName.Clear(); + PostQuitMessage(0); + continue; + } + + int commandId = unchecked((int)message.WParam.ToUInt64()); + if (!_commands.TryRemove(commandId, out Command command)) continue; + try + { + if (message.MessageId == WmAppRegister) + { + Registration registration = command.Registration; + command.Result = RegisterHotKey(IntPtr.Zero, registration.Id, registration.Gesture.Modifiers, registration.Gesture.VirtualKey) != 0; + if (command.Result) + { + _byName[registration.Name] = registration; + _byId[registration.Id] = registration; + } + } + else if (message.MessageId == WmAppUnregister) + { + command.Result = true; + if (_byName.Remove(command.Name, out Registration registration)) + { + _byId.Remove(registration.Id); + command.Result = UnregisterHotKey(IntPtr.Zero, registration.Id) != 0; + } + } + } + finally { command.Completed.Set(); } + } + } + + private sealed record Registration(string Name, int Id, HotkeyGesture Gesture, Action Callback); + + private sealed class Command + { + public Command(Registration registration) => Registration = registration; + public Command(string name) => Name = name; + public Registration Registration { get; } + public string Name { get; } + public ManualResetEventSlim Completed { get; } = new(false); + public bool Result { get; set; } + } + + [StructLayout(LayoutKind.Sequential)] + private struct Point { public int X; public int Y; } + [StructLayout(LayoutKind.Sequential)] + private struct NativeMessage + { + public IntPtr HWnd; public uint MessageId; public UIntPtr WParam; public IntPtr LParam; + public uint Time; public Point Pt; public uint LPrivate; + } + + [LibraryImport("user32.dll", EntryPoint = "RegisterHotKey", SetLastError = true)] + private static partial int RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + + [LibraryImport("user32.dll", EntryPoint = "UnregisterHotKey", SetLastError = true)] + private static partial int UnregisterHotKey(IntPtr hWnd, int id); + + [LibraryImport("user32.dll", EntryPoint = "GetMessageW")] + private static partial int GetMessage(out NativeMessage msg, IntPtr hWnd, uint min, uint max); + + [LibraryImport("user32.dll", EntryPoint = "PeekMessageW")] + private static partial int PeekMessage(out NativeMessage msg, IntPtr hWnd, uint min, uint max, uint removeMsg); + + [LibraryImport("user32.dll", EntryPoint = "PostThreadMessageW", SetLastError = true)] + private static partial int PostThreadMessage(uint threadId, uint msg, UIntPtr wParam, IntPtr lParam); + + [LibraryImport("user32.dll", EntryPoint = "PostQuitMessage")] + private static partial void PostQuitMessage(int exitCode); + + [LibraryImport("kernel32.dll", EntryPoint = "GetCurrentThreadId")] + private static partial uint GetCurrentThreadId(); + } +} diff --git a/Shadowsocks.Windows/Controller/System/ProcessSingleInstanceGuard.cs b/Shadowsocks.Windows/Controller/System/ProcessSingleInstanceGuard.cs new file mode 100644 index 000000000..bceeecf1a --- /dev/null +++ b/Shadowsocks.Windows/Controller/System/ProcessSingleInstanceGuard.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; + +namespace Shadowsocks.Controller +{ + /// + /// Process-wide single-instance gate that is independent of Windows App SDK + /// application identity. This is required because the verified Start-with-Windows + /// copy lives at a different executable path from the user-facing product EXE. + /// + internal sealed class ProcessSingleInstanceGuard : IDisposable + { + internal const string DefaultMutexName = @"Local\Shadowsocks.Reborn.WinUI.Process"; + + private Mutex mutex; + private bool ownsMutex; + + private ProcessSingleInstanceGuard(Mutex mutex) + { + this.mutex = mutex; + ownsMutex = true; + } + + internal static ProcessSingleInstanceGuard TryAcquire(string mutexName = DefaultMutexName) + { + if (string.IsNullOrWhiteSpace(mutexName)) + { + throw new ArgumentException("A single-instance mutex name is required.", nameof(mutexName)); + } + + var candidate = new Mutex(initiallyOwned: false, mutexName); + try + { + bool acquired; + try + { + acquired = candidate.WaitOne(0, exitContext: false); + } + catch (AbandonedMutexException) + { + acquired = true; + } + + if (!acquired) + { + candidate.Dispose(); + return null; + } + + return new ProcessSingleInstanceGuard(candidate); + } + catch + { + candidate.Dispose(); + throw; + } + } + + public void Dispose() + { + Mutex current = mutex; + if (current is null) + { + return; + } + + mutex = null; + if (ownsMutex) + { + ownsMutex = false; + current.ReleaseMutex(); + } + current.Dispose(); + } + } +} diff --git a/Shadowsocks.Windows/Controller/System/ProtocolHandler.cs b/Shadowsocks.Windows/Controller/System/ProtocolHandler.cs new file mode 100644 index 000000000..221b02130 --- /dev/null +++ b/Shadowsocks.Windows/Controller/System/ProtocolHandler.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.Win32; +using NLog; +using Shadowsocks.Core; + +namespace Shadowsocks.Controller +{ + public static class ProtocolHandler + { + private const string SsUrlRegKey = @"SOFTWARE\Classes\ss"; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static bool Set(bool enabled) + { + try + { + if (!enabled) + { + Registry.CurrentUser.DeleteSubKeyTree(SsUrlRegKey, throwOnMissingSubKey: false); + Logger.Info(@"Successfully removed ss:// association."); + return true; + } + + using RegistryKey ssUrlAssociation = Registry.CurrentUser.CreateSubKey( + SsUrlRegKey, + RegistryKeyPermissionCheck.ReadWriteSubTree); + + if (ssUrlAssociation is null) + { + Logger.Error(@"Failed to create HKCU\SOFTWARE\Classes\ss to register ss:// association."); + return false; + } + + ssUrlAssociation.SetValue("", "URL:shadowsocks-reborn"); + ssUrlAssociation.SetValue("URL Protocol", ""); + + using RegistryKey shell = ssUrlAssociation.CreateSubKey("shell"); + using RegistryKey open = shell?.CreateSubKey("open"); + using RegistryKey command = open?.CreateSubKey("command"); + if (command is null) + { + Logger.Error(@"Failed to create the ss:// shell open command registry key."); + return false; + } + + command.SetValue("", BuildShellOpenCommand(RuntimeEnvironment.ExecutablePath)); + Logger.Info(@"Successfully added ss:// association."); + return true; + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + return false; + } + } + + public static bool Check() + { + try + { + using RegistryKey ssUrlAssociation = Registry.CurrentUser.OpenSubKey(SsUrlRegKey, writable: false); + using RegistryKey command = ssUrlAssociation? + .OpenSubKey("shell", writable: false)? + .OpenSubKey("open", writable: false)? + .OpenSubKey("command", writable: false); + + return command?.GetValue("") is string registeredCommand + && string.Equals( + registeredCommand, + BuildShellOpenCommand(RuntimeEnvironment.ExecutablePath), + StringComparison.Ordinal); + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + return false; + } + } + + internal static string BuildShellOpenCommand(string executablePath) + { + if (string.IsNullOrWhiteSpace(executablePath)) + { + throw new ArgumentException("Executable path is required.", nameof(executablePath)); + } + + // Windows protocol handlers are parsed as command lines. Quote both the + // executable path and the URL placeholder so paths/URLs containing spaces + // cannot change argument boundaries. + return $"\"{executablePath}\" --open-url \"%1\""; + } + } +} diff --git a/Shadowsocks.Windows/Controller/System/SystemProxy.cs b/Shadowsocks.Windows/Controller/System/SystemProxy.cs new file mode 100644 index 000000000..2e5814e06 --- /dev/null +++ b/Shadowsocks.Windows/Controller/System/SystemProxy.cs @@ -0,0 +1,115 @@ +using System; +using System.ComponentModel; +using Shadowsocks.Core; +using NLog; +using Shadowsocks.Model; +using Shadowsocks.Util.SystemProxy; + +namespace Shadowsocks.Controller +{ + public enum SystemProxyMode + { + Disabled, + Global, + Pac, + } + + public static class SystemProxy + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + internal static SystemProxyMode GetCurrentMode(Configuration config, PACServer pacSrv) + { + if (!config.Enabled || !WinINet.Operational) + { + return SystemProxyMode.Disabled; + } + + try + { + WinINetSetting setting = WinINet.Query(); + if (config.global) + { + string expectedServer = $"localhost:{config.localPort}"; + return setting.Flags.HasFlag(InternetPerConnectionFlags.Proxy) && + string.Equals(setting.ProxyServer, expectedServer, StringComparison.OrdinalIgnoreCase) + ? SystemProxyMode.Global + : SystemProxyMode.Disabled; + } + + string expectedPacUrl = pacSrv?.PacUrl; + + return !string.IsNullOrEmpty(expectedPacUrl) && + setting.Flags.HasFlag(InternetPerConnectionFlags.AutoProxyUrl) && + string.Equals(setting.AutoConfigUrl, expectedPacUrl, StringComparison.OrdinalIgnoreCase) + ? SystemProxyMode.Pac + : SystemProxyMode.Disabled; + } + catch (Win32Exception ex) + { + logger.LogUsefulException(ex); + return SystemProxyMode.Disabled; + } + } + + public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, IUserInteractionService interaction = null, bool noRetry = false) + { + interaction ??= NullUserInteractionService.Instance; + bool enabled = config.Enabled && !forceDisable; + + try + { + if (!WinINet.Operational) + { + throw new InvalidOperationException("WinINet system proxy integration is unavailable."); + } + + if (!enabled) + { + WinINet.Restore(); + return; + } + + if (config.global) + { + WinINet.ProxyGlobal($"localhost:{config.localPort}"); + return; + } + + string pacUrl = pacSrv?.PacUrl ?? throw new InvalidOperationException("PAC server is not initialized."); + + // Both Local PAC and Online PAC are served from localhost. For Online PAC + // this points at the persistent cache maintained by OnlinePacCache, so + // WinINet never needs direct access to the remote PAC URL. + WinINet.ProxyPAC(pacUrl); + } + catch (Exception ex) when (ex is Win32Exception or InvalidOperationException) + { + logger.LogUsefulException(ex); + if (!noRetry) + { + bool retry = interaction.Confirm( + I18N.GetString("Error occured when process proxy setting, do you want reset current setting and retry?"), + I18N.GetString("shadowsocks-reborn")); + if (retry) + { + try + { + WinINet.Reset(); + Update(config, forceDisable, pacSrv, interaction, true); + return; + } + catch (Exception retryException) + { + logger.LogUsefulException(retryException); + } + } + } + + interaction.ShowError( + I18N.GetString("Unrecoverable proxy setting error occured, see log for detail"), + I18N.GetString("shadowsocks-reborn")); + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/AdminCaptureManager.cs b/Shadowsocks.Windows/Controller/Traffic/AdminCaptureManager.cs new file mode 100644 index 000000000..8720f4cc0 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/AdminCaptureManager.cs @@ -0,0 +1,755 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; +using Shadowsocks.Core; +using Shadowsocks.Model; +using Shadowsocks.Routing; +using Shadowsocks.Controller.Service; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Traffic +{ + internal sealed class AdminElevationCanceledException(string message, Exception innerException) + : OperationCanceledException(message, innerException) + { + } + + /// + /// Owns the elevated WinDivert broker. The broker is intentionally separate from + /// the UI process. In Game Mode it terminates its capture child and removes the + /// WinDivert driver while keeping the elevated broker alive, allowing Admin Mode + /// to resume later without another UAC prompt. + /// + internal sealed class AdminCaptureManager : IAsyncDisposable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly SemaphoreSlim _gate = new(1, 1); + private readonly WinDivertInstaller _installer = new(); + private readonly CancellationTokenSource _healthShutdown = new(); + private readonly Task _healthLoop; + + private Process _brokerProcess; + private NetworkServiceRuntime.HelperLease _helperLease; + private NamedPipeServerStream _pipe; + private StreamReader _reader; + private StreamWriter _writer; + private string _lastStartRequest; + private bool _captureActive; + private bool _gameMode; + private int _tcpRedirectPort; + private int _udpRedirectPort; + private bool _dnsInterceptionActive; + private bool _dnsFailClosedActive; + private bool _managedRoutingActive; + private int _managedRoutingRuleCount; + private bool _disposed; + + public AdminCaptureManager() + { + _healthLoop = Task.Run(() => HealthLoopAsync(_healthShutdown.Token)); + } + + public bool IsBrokerRunning => _brokerProcess is { HasExited: false } && _pipe is { IsConnected: true }; + public bool IsCaptureActive => _captureActive && IsBrokerRunning; + public bool IsGameMode => _gameMode; + public int TcpRedirectPort => IsCaptureActive ? _tcpRedirectPort : 0; + public int UdpRedirectPort => IsCaptureActive ? _udpRedirectPort : 0; + public bool DnsInterceptionActive => IsCaptureActive && _dnsInterceptionActive; + public bool DnsFailClosedActive => IsCaptureActive && _dnsFailClosedActive; + public bool ManagedRoutingActive => IsCaptureActive && _managedRoutingActive; + public int ManagedRoutingRuleCount => IsCaptureActive ? _managedRoutingRuleCount : 0; + + public event EventHandler StatusChanged; + + public async Task StartOrUpdateAsync( + Configuration configuration, + IEnumerable excludedProcessIds, + DnsCaptureRuntimeState dnsRuntime, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(configuration); + + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + + string winDivertDirectory = await _installer + .EnsureInstalledAsync(configuration, cancellationToken) + .ConfigureAwait(false); + _lastStartRequest = BuildStartRequest(configuration, winDivertDirectory, excludedProcessIds, dnsRuntime); + + await EnsureBrokerAsync(cancellationToken).ConfigureAwait(false); + if (_gameMode) + { + // The configuration is remembered by the broker, but no driver/capture + // process is started until Game Mode ends. + JObject gameModeResponse = await SendRawAsync( + BuildControlRequest("restart", configuration, winDivertDirectory, excludedProcessIds, dnsRuntime), + cancellationToken).ConfigureAwait(false); + EnsureSuccess(gameModeResponse, "Unable to update Admin Mode while Game Mode is active."); + _captureActive = false; + RaiseStatusChanged(); + return; + } + + string command = IsCaptureActive ? "restart" : "start"; + try + { + JObject response = await SendRawAsync( + command == "start" + ? _lastStartRequest + : BuildControlRequest("restart", configuration, winDivertDirectory, excludedProcessIds, dnsRuntime), + cancellationToken).ConfigureAwait(false); + EnsureSuccess(response, "Unable to activate Admin Mode."); + EnsureCaptureActive(response, "WinDivert capture child did not become active."); + UpdateRedirectPorts(response); + LogCaptureConfirmed(response, command); + _captureActive = true; + } + catch + { + _captureActive = false; + ClearRedirectPorts(); + RaiseStatusChanged(); + throw; + } + + RaiseStatusChanged(); + } + finally + { + _gate.Release(); + } + } + + public async Task EnterGameModeAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + if (IsBrokerRunning) + { + JObject response = await SendCommandAsync("game-on", cancellationToken).ConfigureAwait(false); + EnsureSuccess(response, "Unable to disable WinDivert for Game Mode."); + } + + _gameMode = true; + _captureActive = false; + ClearRedirectPorts(); + RaiseStatusChanged(); + } + finally + { + _gate.Release(); + } + } + + public async Task ExitGameModeAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + if (!IsBrokerRunning || string.IsNullOrWhiteSpace(_lastStartRequest)) + { + _gameMode = false; + _captureActive = false; + RaiseStatusChanged(); + return; + } + + JObject response = await SendCommandAsync("game-off", cancellationToken).ConfigureAwait(false); + EnsureSuccess(response, "Unable to restore Admin Mode after Game Mode."); + EnsureCaptureActive(response, "WinDivert capture child did not become active after Game Mode."); + UpdateRedirectPorts(response); + LogCaptureConfirmed(response, "game-off"); + _gameMode = false; + _captureActive = true; + RaiseStatusChanged(); + } + finally + { + _gate.Release(); + } + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_disposed) + { + return; + } + + if (IsBrokerRunning) + { + try + { + await SendCommandAsync("stop", cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + Logger.Debug(exception, "Elevated network broker did not stop cleanly."); + } + } + + CleanupBroker(); + _captureActive = false; + _gameMode = false; + ClearRedirectPorts(); + RaiseStatusChanged(); + } + finally + { + _gate.Release(); + } + } + + private async Task EnsureBrokerAsync(CancellationToken cancellationToken) + { + if (IsBrokerRunning) + { + return; + } + + CleanupBroker(); + _helperLease = NetworkServiceRuntime.AcquireHelper(); + string helperPath = _helperLease.Path; + string pipeName = "Shadowsocks.NetworkService." + Guid.NewGuid().ToString("N"); + _pipe = new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.WriteThrough); + + ProcessStartInfo startInfo = new() + { + FileName = helperPath, + Arguments = $"--pipe {pipeName}", + WorkingDirectory = Path.GetDirectoryName(helperPath) ?? Shadowsocks.Core.RuntimeEnvironment.WorkingDirectory, + UseShellExecute = true, + Verb = "runas", + WindowStyle = ProcessWindowStyle.Hidden, + }; + + ValidateFrameworkDependentHelperFiles(helperPath); + + Directory.CreateDirectory(AppStoragePaths.TempStartupLogsRoot); + string startupLogPath = Path.Combine( + AppStoragePaths.TempStartupLogsRoot, + $"Shadowsocks.NetworkService.{pipeName}.startup.log"); + startInfo.Arguments += $" --startup-log \"{startupLogPath}\""; + + try + { + _brokerProcess = Process.Start(startInfo) + ?? throw new InvalidOperationException("Unable to start the elevated network broker."); + } + catch (Win32Exception exception) when (exception.NativeErrorCode == 1223) + { + CleanupBroker(); + throw new AdminElevationCanceledException("Administrator elevation was cancelled.", exception); + } + + try + { + Task connectTask = _pipe.WaitForConnectionAsync(cancellationToken); + Task exitTask = _brokerProcess.WaitForExitAsync(cancellationToken); + Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + Task completed = await Task.WhenAny(connectTask, exitTask, timeoutTask).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + if (completed == connectTask) + { + await connectTask.ConfigureAwait(false); + } + else if (completed == exitTask) + { + await exitTask.ConfigureAwait(false); + int exitCode = _brokerProcess.ExitCode; + string detail = ReadStartupFailure(startupLogPath); + const int appPathFindFailure = unchecked((int)0x8000809A); + if (exitCode == appPathFindFailure) + { + throw new InvalidOperationException( + "Shadowsocks.NetworkService.exe is a framework-dependent .NET apphost, but its " + + "Shadowsocks.NetworkService.dll is not beside it (host error 0x8000809A). " + + "This usually means a stale framework-dependent helper was copied into the publish directory. " + + "Republish the application with the current x64 profile."); + } + + throw new InvalidOperationException( + string.IsNullOrWhiteSpace(detail) + ? $"Elevated network broker exited before connecting to the control pipe (exit code {exitCode}). " + + "For a framework-dependent Debug build, Shadowsocks.NetworkService.runtimeconfig.json, .deps.json and .dll must be beside the helper executable." + : $"Elevated network broker exited before connecting to the control pipe (exit code {exitCode}). {detail}"); + } + else + { + string detail = ReadStartupFailure(startupLogPath); + throw new TimeoutException( + string.IsNullOrWhiteSpace(detail) + ? "Timed out waiting for the elevated network broker to connect to the control pipe." + : $"Timed out waiting for the elevated network broker to connect to the control pipe. {detail}"); + } + } + catch + { + CleanupBroker(); + throw; + } + finally + { + TryDelete(startupLogPath); + } + + _reader = new StreamReader(_pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true); + _writer = new StreamWriter(_pipe, new UTF8Encoding(false), bufferSize: 4096, leaveOpen: true) + { + AutoFlush = true, + }; + + try + { + JObject pingResponse = await SendCommandAsync("ping", cancellationToken).ConfigureAwait(false); + EnsureSuccess(pingResponse, "Unable to validate the elevated NetworkService helper."); + ValidateHelperVersion(pingResponse); + } + catch + { + CleanupBroker(); + throw; + } + } + + private async Task HealthLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false); + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + + try + { + if (_disposed || _gameMode) + { + continue; + } + + if (!IsBrokerRunning) + { + if (_captureActive) + { + _captureActive = false; + ClearRedirectPorts(); + RaiseStatusChanged(); + } + continue; + } + + if (!_captureActive && string.IsNullOrWhiteSpace(_lastStartRequest)) + { + continue; + } + + bool wasActive = _captureActive; + int oldTcpPort = _tcpRedirectPort; + int oldUdpPort = _udpRedirectPort; + bool oldDnsInterception = _dnsInterceptionActive; + bool oldDnsFailClosed = _dnsFailClosedActive; + try + { + JObject response = await SendCommandAsync("ping", cancellationToken).ConfigureAwait(false); + EnsureSuccess(response, "Unable to validate the active WinDivert capture child."); + bool captureActive = response.Value("captureActive") == true; + if (captureActive) + { + UpdateRedirectPorts(response); + _captureActive = true; + } + else + { + _captureActive = false; + ClearRedirectPorts(); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception exception) + { + Logger.Warn(exception, "Elevated WinDivert capture health check failed."); + _captureActive = false; + ClearRedirectPorts(); + } + + if (wasActive != _captureActive + || oldTcpPort != _tcpRedirectPort + || oldUdpPort != _udpRedirectPort + || oldDnsInterception != _dnsInterceptionActive + || oldDnsFailClosed != _dnsFailClosedActive) + { + RaiseStatusChanged(); + } + } + finally + { + _gate.Release(); + } + } + } + + private async Task SendCommandAsync(string command, CancellationToken cancellationToken) + { + return await SendRawAsync( + JsonConvert.SerializeObject(new { command }), + cancellationToken).ConfigureAwait(false); + } + + private async Task SendRawAsync(string json, CancellationToken cancellationToken) + { + if (!IsBrokerRunning || _reader is null || _writer is null) + { + throw new InvalidOperationException("Elevated network broker is not connected."); + } + + await _writer.WriteLineAsync(json).ConfigureAwait(false); + Task readTask = _reader.ReadLineAsync(cancellationToken).AsTask(); + Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + Task completed = await Task.WhenAny(readTask, timeoutTask).ConfigureAwait(false); + if (completed != readTask) + { + cancellationToken.ThrowIfCancellationRequested(); + CleanupBroker(); + _captureActive = false; + throw new TimeoutException("Timed out waiting for the elevated network broker."); + } + + string line = await readTask.ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(line)) + { + CleanupBroker(); + throw new IOException("Elevated network broker disconnected unexpectedly."); + } + + try + { + return JObject.Parse(line); + } + catch (JsonReaderException) + { + CleanupBroker(); + throw; + } + } + + private static string BuildControlRequest( + string command, + Configuration configuration, + string winDivertDirectory, + IEnumerable excludedProcessIds, + DnsCaptureRuntimeState dnsRuntime) + { + JObject request = JObject.Parse(BuildStartRequest(configuration, winDivertDirectory, excludedProcessIds, dnsRuntime)); + request["command"] = command; + return request.ToString(Formatting.None); + } + + internal static string BuildStartRequest( + Configuration configuration, + string winDivertDirectory, + IEnumerable excludedProcessIds, + DnsCaptureRuntimeState dnsRuntime) + { + int fallback = configuration.Enabled && configuration.global + ? (int)TrafficRouteAction.Proxy + : (int)TrafficRouteAction.Direct; + var rules = (configuration.applicationRules ?? []) + .Where(rule => rule is not null) + .Select(rule => new + { + enabled = rule.Enabled, + application = rule.Application ?? string.Empty, + action = (int)rule.Action, + }) + .ToArray(); + + ManagedRoutingSnapshot managedRouting = ManagedRoutingSnapshotBuilder.Build(configuration); + + var request = new + { + command = "start", + winDivertDirectory, + localProxyHost = NormalizeLoopbackHost(configuration.LocalHost), + localProxyPort = configuration.localPort, + mainProcessId = Environment.ProcessId, + defaultRoute = fallback, + applicationRules = rules, + excludedProcessIds = (excludedProcessIds ?? []) + .Where(processId => processId > 0) + .Append(dnsRuntime.ProcessId) + .Where(processId => processId > 0) + .Distinct() + .ToArray(), + managedRouting = new + { + enabled = managedRouting.IsEnabled, + source = managedRouting.Source, + defaultRules = managedRouting.DefaultRules, + userRules = managedRouting.UserRules, + }, + dnsPolicy = new + { + mode = (int)(configuration.dnsPolicy?.mode ?? DnsPolicyMode.System), + directDnsServer = configuration.dnsPolicy?.directDnsServer ?? string.Empty, + directDnsFallbackServer = configuration.dnsPolicy?.directDnsFallbackServer ?? string.Empty, + directDnsRouteThroughShadowsocks = configuration.dnsPolicy?.directDnsRouteThroughShadowsocks ?? false, + customDohUrl = configuration.dnsPolicy?.customDohUrl ?? string.Empty, + customDohRouteThroughShadowsocks = configuration.dnsPolicy?.customDohRouteThroughShadowsocks ?? false, + dnsCryptPort = dnsRuntime.IsReady ? dnsRuntime.Port : 0, + dnsCryptProcessId = dnsRuntime.IsReady ? dnsRuntime.ProcessId : 0, + dnsCryptComponentRoot = AppStoragePaths.DnsCryptComponentDirectory, + failClosed = (configuration.dnsPolicy?.mode ?? DnsPolicyMode.System) == DnsPolicyMode.DnsCrypt + ? true + : configuration.dnsPolicy?.dnsCrypt?.failClosed ?? true, + }, + }; + return JsonConvert.SerializeObject(request); + } + + private static string NormalizeLoopbackHost(string host) + { + if (string.IsNullOrWhiteSpace(host)) + { + return "127.0.0.1"; + } + + return host.Trim().TrimStart('[').TrimEnd(']'); + } + + private static void ValidateHelperVersion(JObject response) + { + string helperVersionText = response.Value("version") ?? string.Empty; + if (!System.Version.TryParse(helperVersionText, out System.Version helperVersion) + || !System.Version.TryParse(ApplicationInfo.Version, out System.Version expectedVersion) + || helperVersion != expectedVersion) + { + throw new InvalidOperationException( + $"Shadowsocks.NetworkService version mismatch. Expected {ApplicationInfo.Version}, got " + + $"{(string.IsNullOrWhiteSpace(helperVersionText) ? "" : helperVersionText)}."); + } + + Logger.Info("NetworkService helper protocol version confirmed: {0}.", helperVersion); + } + + private static void EnsureSuccess(JObject response, string prefix) + { + if (response.Value("success") == true) + { + return; + } + + string message = response.Value("message"); + throw new InvalidOperationException(string.IsNullOrWhiteSpace(message) ? prefix : $"{prefix} {message}"); + } + + private static void EnsureCaptureActive(JObject response, string message) + { + if (response.Value("captureActive") == true) + { + return; + } + + throw new InvalidOperationException(message); + } + + private static void ValidateFrameworkDependentHelperFiles(string helperPath) + { + string directory = Path.GetDirectoryName(helperPath) ?? Shadowsocks.Core.RuntimeEnvironment.WorkingDirectory; + string baseName = Path.GetFileNameWithoutExtension(helperPath); + string managedDll = Path.Combine(directory, baseName + ".dll"); + if (!File.Exists(managedDll)) + { + // Product single-file publishing intentionally has no managed sidecars. + return; + } + + string[] required = + [ + managedDll, + Path.Combine(directory, baseName + ".deps.json"), + Path.Combine(directory, baseName + ".runtimeconfig.json"), + ]; + string[] missing = required.Where(path => !File.Exists(path)).ToArray(); + if (missing.Length > 0) + { + throw new FileNotFoundException( + "The framework-dependent Shadowsocks.NetworkService build is incomplete. Missing: " + + string.Join(", ", missing.Select(Path.GetFileName)) + + ". Rebuild the complete solution or publish the NetworkService product helper.", + missing[0]); + } + } + + private static string ReadStartupFailure(string path) + { + try + { + return File.Exists(path) ? File.ReadAllText(path).Trim() : string.Empty; + } + catch + { + return string.Empty; + } + } + + private static void TryDelete(string path) + { + try + { + File.Delete(path); + } + catch + { + } + } + + private void CleanupBroker() + { + _captureActive = false; + ClearRedirectPorts(); + + try + { + _writer?.Dispose(); + _reader?.Dispose(); + _pipe?.Dispose(); + } + catch + { + } + + _writer = null; + _reader = null; + _pipe = null; + + if (_brokerProcess is not null) + { + try + { + if (!_brokerProcess.HasExited) + { + _brokerProcess.Kill(entireProcessTree: true); + } + } + catch + { + } + + _brokerProcess.Dispose(); + _brokerProcess = null; + } + + _helperLease?.Dispose(); + _helperLease = null; + } + + private void UpdateRedirectPorts(JObject response) + { + _tcpRedirectPort = Math.Max(0, response.Value("tcpRedirectPort") ?? 0); + _udpRedirectPort = Math.Max(0, response.Value("udpRedirectPort") ?? 0); + _dnsInterceptionActive = response.Value("dnsInterceptionActive") == true; + _dnsFailClosedActive = response.Value("dnsFailClosedActive") == true; + _managedRoutingActive = response.Value("managedRoutingActive") == true; + _managedRoutingRuleCount = Math.Max(0, response.Value("managedRoutingRuleCount") ?? 0); + } + + private void ClearRedirectPorts() + { + _tcpRedirectPort = 0; + _udpRedirectPort = 0; + _dnsInterceptionActive = false; + _dnsFailClosedActive = false; + _managedRoutingActive = false; + _managedRoutingRuleCount = 0; + } + + private static void LogCaptureConfirmed(JObject response, string operation) + { + int tcpPort = response.Value("tcpRedirectPort") ?? 0; + int udpPort = response.Value("udpRedirectPort") ?? 0; + bool dnsIntercept = response.Value("dnsInterceptionActive") == true; + bool dnsFailClosed = response.Value("dnsFailClosedActive") == true; + bool managedRouting = response.Value("managedRoutingActive") == true; + int managedRuleCount = response.Value("managedRoutingRuleCount") ?? 0; + string message = response.Value("message") ?? "capture-ready"; + Logger.Info( + "WinDivert capture confirmed ({0}): {1}; TCP redirect port={2}, UDP redirect port={3}, " + + "DNS intercept={4}, DNS fail-closed={5}, managed routing={6} ({7} rules). " + + "The elevated capture child returned success only after WinDivertOpen completed.", + operation, + message, + tcpPort, + udpPort, + dnsIntercept, + dnsFailClosed, + managedRouting, + managedRuleCount); + } + + private void RaiseStatusChanged() + { + StatusChanged?.Invoke(this, EventArgs.Empty); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + await StopAsync().ConfigureAwait(false); + _disposed = true; + _healthShutdown.Cancel(); + try + { + await _healthLoop.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + _healthShutdown.Dispose(); + _installer.Dispose(); + _gate.Dispose(); + } + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/Applications/GameDiscoveryService.cs b/Shadowsocks.Windows/Controller/Traffic/Applications/GameDiscoveryService.cs new file mode 100644 index 000000000..4ea7989c9 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/Applications/GameDiscoveryService.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using NLog; + +namespace Shadowsocks.Controller.Traffic.Applications +{ + public sealed class DiscoveredGame + { + public DiscoveredGame(string name, string executablePath, string source) + { + Name = name ?? string.Empty; + ExecutablePath = executablePath ?? string.Empty; + Source = source ?? string.Empty; + } + + public string Name { get; } + public string ExecutablePath { get; } + public string Source { get; } + } + + /// + /// Best-effort local game discovery for Game Mode suggestions. It only reads + /// launcher manifests/registry entries and common game-library directories; + /// it never changes launcher configuration and never replaces manual rules. + /// + public static class GameDiscoveryService + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly Regex VdfPathRegex = new("\\\"path\\\"\\s+\\\"(?[^\\\"]+)\\\"", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex AcfValueRegex = new("\\\"(?[^\\\"]+)\\\"\\s+\\\"(?[^\\\"]*)\\\"", RegexOptions.Compiled); + private static readonly string[] RejectedExecutableTokens = + [ + "unins", "uninstall", "setup", "install", "crash", "report", "launcher", "updater", "update", + "helper", "bootstrap", "redist", "vcredist", "vc_redist", "dxsetup", "easyanticheat", "battleye", + "unitycrashhandler", "cefprocess", "notification", "overlay", "benchmark", "config" + ]; + private static readonly string[] RejectedDirectoryTokens = + [ + "_commonredist", "redist", "redistributable", "installer", "support", "prereq", "crashreporter", + "easyanticheat", "battleye", "dotnet", "directx" + ]; + + public static IReadOnlyList Discover() + { + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + TryDiscoverSteam(results); + TryDiscoverEpic(results); + TryDiscoverGog(results); + TryDiscoverXbox(results); + + return results.Values + .Where(game => File.Exists(game.ExecutablePath)) + .OrderBy(game => game.Name, StringComparer.CurrentCultureIgnoreCase) + .ThenBy(game => game.Source, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private static void TryDiscoverSteam(Dictionary results) + { + try + { + var steamRoots = new HashSet(StringComparer.OrdinalIgnoreCase); + AddRegistryPath(steamRoots, Registry.CurrentUser, @"Software\Valve\Steam", "SteamPath"); + AddRegistryPath(steamRoots, Registry.LocalMachine, @"Software\WOW6432Node\Valve\Steam", "InstallPath"); + AddRegistryPath(steamRoots, Registry.LocalMachine, @"Software\Valve\Steam", "InstallPath"); + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (!string.IsNullOrWhiteSpace(programFilesX86)) + { + steamRoots.Add(Path.Combine(programFilesX86, "Steam")); + } + + var libraries = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string root in steamRoots.Where(Directory.Exists)) + { + libraries.Add(root); + string libraryFile = Path.Combine(root, "steamapps", "libraryfolders.vdf"); + if (!File.Exists(libraryFile)) + { + continue; + } + + string text = File.ReadAllText(libraryFile); + foreach (Match match in VdfPathRegex.Matches(text)) + { + string path = match.Groups["value"].Value.Replace("\\\\", "\\"); + if (!string.IsNullOrWhiteSpace(path)) + { + libraries.Add(path); + } + } + } + + foreach (string library in libraries.Where(Directory.Exists)) + { + string steamApps = Path.Combine(library, "steamapps"); + if (!Directory.Exists(steamApps)) + { + continue; + } + + foreach (string manifest in SafeEnumerateFiles(steamApps, "appmanifest_*.acf", SearchOption.TopDirectoryOnly)) + { + try + { + Dictionary values = ParseAcfValues(File.ReadAllText(manifest)); + if (!values.TryGetValue("installdir", out string installDir) || string.IsNullOrWhiteSpace(installDir)) + { + continue; + } + string name = values.TryGetValue("name", out string manifestName) && !string.IsNullOrWhiteSpace(manifestName) + ? manifestName + : installDir; + string gameDirectory = Path.Combine(steamApps, "common", installDir); + string executable = FindBestExecutable(gameDirectory, name, installDir); + AddResult(results, name, executable, "Steam"); + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to inspect Steam manifest {0}", manifest); + } + } + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Steam game discovery failed."); + } + } + + private static void TryDiscoverEpic(Dictionary results) + { + try + { + string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + string manifests = Path.Combine(programData, "Epic", "EpicGamesLauncher", "Data", "Manifests"); + foreach (string manifest in SafeEnumerateFiles(manifests, "*.item", SearchOption.TopDirectoryOnly)) + { + try + { + JObject json = JObject.Parse(File.ReadAllText(manifest)); + string name = json.Value("DisplayName") ?? json.Value("AppName") ?? Path.GetFileNameWithoutExtension(manifest); + string installLocation = json.Value("InstallLocation") ?? string.Empty; + string launchExecutable = json.Value("LaunchExecutable") ?? string.Empty; + string executable = ResolveExecutable(installLocation, launchExecutable); + if (string.IsNullOrWhiteSpace(executable)) + { + executable = FindBestExecutable(installLocation, name, Path.GetFileName(installLocation)); + } + AddResult(results, name, executable, "Epic Games"); + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to inspect Epic manifest {0}", manifest); + } + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Epic game discovery failed."); + } + } + + private static void TryDiscoverGog(Dictionary results) + { + TryDiscoverGogRegistry(results, Registry.LocalMachine, @"SOFTWARE\WOW6432Node\GOG.com\Games"); + TryDiscoverGogRegistry(results, Registry.LocalMachine, @"SOFTWARE\GOG.com\Games"); + TryDiscoverGogRegistry(results, Registry.CurrentUser, @"SOFTWARE\GOG.com\Games"); + } + + private static void TryDiscoverGogRegistry(Dictionary results, RegistryKey root, string subKeyPath) + { + try + { + using RegistryKey games = root.OpenSubKey(subKeyPath, writable: false); + if (games is null) + { + return; + } + foreach (string subKeyName in games.GetSubKeyNames()) + { + using RegistryKey game = games.OpenSubKey(subKeyName, writable: false); + if (game is null) + { + continue; + } + string name = game.GetValue("gameName")?.ToString() ?? game.GetValue("gameNameBase")?.ToString() ?? subKeyName; + string installDirectory = game.GetValue("path")?.ToString() ?? string.Empty; + string executableValue = game.GetValue("exe")?.ToString() ?? string.Empty; + string executable = ResolveExecutable(installDirectory, executableValue); + if (string.IsNullOrWhiteSpace(executable)) + { + executable = FindBestExecutable(installDirectory, name, Path.GetFileName(installDirectory)); + } + AddResult(results, name, executable, "GOG"); + } + } + catch (Exception exception) + { + Logger.Debug(exception, "GOG game discovery failed for {0}", subKeyPath); + } + } + + private static void TryDiscoverXbox(Dictionary results) + { + try + { + foreach (DriveInfo drive in DriveInfo.GetDrives()) + { + if (!drive.IsReady || drive.DriveType is DriveType.CDRom or DriveType.Network) + { + continue; + } + string xboxRoot = Path.Combine(drive.RootDirectory.FullName, "XboxGames"); + foreach (string gameDirectory in SafeEnumerateDirectories(xboxRoot)) + { + string content = Path.Combine(gameDirectory, "Content"); + string searchRoot = Directory.Exists(content) ? content : gameDirectory; + string name = Path.GetFileName(gameDirectory); + string executable = FindBestExecutable(searchRoot, name, name); + AddResult(results, name, executable, "Xbox"); + } + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Xbox game discovery failed."); + } + } + + private static void AddRegistryPath(HashSet paths, RegistryKey root, string subKey, string valueName) + { + try + { + using RegistryKey key = root.OpenSubKey(subKey, writable: false); + string value = key?.GetValue(valueName)?.ToString(); + if (!string.IsNullOrWhiteSpace(value)) + { + paths.Add(Environment.ExpandEnvironmentVariables(value)); + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to read game-library registry path {0}\\{1}", subKey, valueName); + } + } + + private static Dictionary ParseAcfValues(string text) + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (Match match in AcfValueRegex.Matches(text ?? string.Empty)) + { + string key = match.Groups["key"].Value; + string value = match.Groups["value"].Value.Replace("\\\\", "\\"); + if (!values.ContainsKey(key)) + { + values[key] = value; + } + } + return values; + } + + private static string ResolveExecutable(string installDirectory, string executable) + { + if (string.IsNullOrWhiteSpace(executable)) + { + return string.Empty; + } + string expanded = Environment.ExpandEnvironmentVariables(executable.Trim().Trim('"')); + if (Path.IsPathRooted(expanded)) + { + return File.Exists(expanded) ? Path.GetFullPath(expanded) : string.Empty; + } + if (string.IsNullOrWhiteSpace(installDirectory)) + { + return string.Empty; + } + string combined = Path.GetFullPath(Path.Combine(installDirectory, expanded.Replace('/', Path.DirectorySeparatorChar))); + return File.Exists(combined) ? combined : string.Empty; + } + + private static string FindBestExecutable(string directory, string displayName, string installName) + { + if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) + { + return string.Empty; + } + + string normalizedName = NormalizeName(displayName); + string normalizedInstall = NormalizeName(installName); + string bestPath = string.Empty; + int bestScore = int.MinValue; + + foreach ((string path, int depth) in EnumerateExecutables(directory, maxDepth: 4, maxFiles: 300)) + { + string fileName = Path.GetFileNameWithoutExtension(path); + string normalizedFile = NormalizeName(fileName); + string lower = fileName.ToLowerInvariant(); + if (RejectedExecutableTokens.Any(token => lower.Contains(token, StringComparison.Ordinal))) + { + continue; + } + + int score = 0; + if (!string.IsNullOrWhiteSpace(normalizedName) && normalizedFile == normalizedName) score += 120; + if (!string.IsNullOrWhiteSpace(normalizedInstall) && normalizedFile == normalizedInstall) score += 110; + if (!string.IsNullOrWhiteSpace(normalizedName) && normalizedFile.Contains(normalizedName, StringComparison.Ordinal)) score += 45; + if (!string.IsNullOrWhiteSpace(normalizedInstall) && normalizedFile.Contains(normalizedInstall, StringComparison.Ordinal)) score += 40; + if (lower.Contains("shipping", StringComparison.Ordinal)) score += 25; + if (path.Contains("Binaries", StringComparison.OrdinalIgnoreCase)) score += 12; + if (path.Contains("Win64", StringComparison.OrdinalIgnoreCase) || path.Contains("x64", StringComparison.OrdinalIgnoreCase)) score += 10; + score += Math.Max(0, 15 - depth * 3); + + if (score > bestScore) + { + bestScore = score; + bestPath = path; + } + } + return bestPath; + } + + private static IEnumerable<(string Path, int Depth)> EnumerateExecutables(string root, int maxDepth, int maxFiles) + { + var queue = new Queue<(string Directory, int Depth)>(); + queue.Enqueue((root, 0)); + int yielded = 0; + while (queue.Count > 0 && yielded < maxFiles) + { + (string current, int depth) = queue.Dequeue(); + foreach (string file in SafeEnumerateFiles(current, "*.exe", SearchOption.TopDirectoryOnly)) + { + yield return (file, depth); + yielded++; + if (yielded >= maxFiles) yield break; + } + if (depth >= maxDepth) + { + continue; + } + foreach (string child in SafeEnumerateDirectories(current)) + { + string name = Path.GetFileName(child).ToLowerInvariant(); + if (RejectedDirectoryTokens.Any(token => name.Contains(token, StringComparison.Ordinal))) + { + continue; + } + queue.Enqueue((child, depth + 1)); + } + } + } + + private static string[] SafeEnumerateFiles(string directory, string pattern, SearchOption option) + { + if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) + { + return Array.Empty(); + } + try + { + return Directory.EnumerateFiles(directory, pattern, option).ToArray(); + } + catch + { + return Array.Empty(); + } + } + + private static string[] SafeEnumerateDirectories(string directory) + { + if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) + { + return Array.Empty(); + } + try + { + return Directory.EnumerateDirectories(directory).ToArray(); + } + catch + { + return Array.Empty(); + } + } + + private static string NormalizeName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + return new string(value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray()); + } + + private static void AddResult(Dictionary results, string name, string executable, string source) + { + if (string.IsNullOrWhiteSpace(executable) || !File.Exists(executable)) + { + return; + } + string fullPath; + try + { + fullPath = Path.GetFullPath(executable); + } + catch + { + return; + } + if (!results.ContainsKey(fullPath)) + { + results[fullPath] = new DiscoveredGame( + string.IsNullOrWhiteSpace(name) ? Path.GetFileNameWithoutExtension(fullPath) : name.Trim(), + fullPath, + source); + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/Applications/TcpProcessResolver.cs b/Shadowsocks.Windows/Controller/Traffic/Applications/TcpProcessResolver.cs new file mode 100644 index 000000000..956d9eb17 --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/Applications/TcpProcessResolver.cs @@ -0,0 +1,272 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Shadowsocks.Controller.Traffic.Applications +{ + /// + /// Resolves a local TCP connection to its owning process without a driver. + /// This is used by User Mode after the application has connected to the local + /// HTTP proxy. Protected processes may expose PID/name but not their full path. + /// + public sealed class TcpProcessResolver + { + private const int AfInet = 2; + private const int AfInet6 = 23; + private const uint ErrorInsufficientBuffer = 122; + + public static ProcessIdentity Resolve(Socket acceptedSocket) + { + if (acceptedSocket?.RemoteEndPoint is not IPEndPoint client + || acceptedSocket.LocalEndPoint is not IPEndPoint proxy) + { + return ProcessIdentity.Unknown; + } + + int processId = client.AddressFamily switch + { + AddressFamily.InterNetwork => FindOwnerIpv4(client, proxy), + AddressFamily.InterNetworkV6 => FindOwnerIpv6(client, proxy), + _ => 0, + }; + + return GetIdentity(processId); + } + + private static int FindOwnerIpv4(IPEndPoint client, IPEndPoint proxy) + { + IntPtr buffer = IntPtr.Zero; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedTcpTable( + IntPtr.Zero, + ref size, + false, + AfInet, + TcpTableClass.TcpTableOwnerPidAll, + 0); + + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedTcpTable( + buffer, + ref size, + false, + AfInet, + TcpTableClass.TcpTableOwnerPidAll, + 0); + if (result != 0) + { + return 0; + } + + int count = Marshal.ReadInt32(buffer); + IntPtr rowPtr = IntPtr.Add(buffer, sizeof(uint)); + int rowSize = Marshal.SizeOf(); + uint clientAddress = ToIpv4UInt32(client.Address); + uint proxyAddress = ToIpv4UInt32(proxy.Address); + + for (int i = 0; i < count; i++) + { + MibTcpRowOwnerPid row = Marshal.PtrToStructure(rowPtr); + if (row.LocalAddr == clientAddress + && DecodePort(row.LocalPort) == client.Port + && row.RemoteAddr == proxyAddress + && DecodePort(row.RemotePort) == proxy.Port) + { + return unchecked((int)row.OwningPid); + } + + rowPtr = IntPtr.Add(rowPtr, rowSize); + } + } + finally + { + if (buffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static int FindOwnerIpv6(IPEndPoint client, IPEndPoint proxy) + { + IntPtr buffer = IntPtr.Zero; + try + { + int size = 0; + uint result = NativeMethods.GetExtendedTcpTable( + IntPtr.Zero, + ref size, + false, + AfInet6, + TcpTableClass.TcpTableOwnerPidAll, + 0); + + if (result != ErrorInsufficientBuffer || size <= 0) + { + return 0; + } + + buffer = Marshal.AllocHGlobal(size); + result = NativeMethods.GetExtendedTcpTable( + buffer, + ref size, + false, + AfInet6, + TcpTableClass.TcpTableOwnerPidAll, + 0); + if (result != 0) + { + return 0; + } + + int count = Marshal.ReadInt32(buffer); + IntPtr rowPtr = IntPtr.Add(buffer, sizeof(uint)); + int rowSize = Marshal.SizeOf(); + byte[] clientAddress = client.Address.GetAddressBytes(); + byte[] proxyAddress = proxy.Address.GetAddressBytes(); + + for (int i = 0; i < count; i++) + { + MibTcp6RowOwnerPid row = Marshal.PtrToStructure(rowPtr); + if (row.LocalAddr.AsSpan().SequenceEqual(clientAddress) + && DecodePort(row.LocalPort) == client.Port + && row.RemoteAddr.AsSpan().SequenceEqual(proxyAddress) + && DecodePort(row.RemotePort) == proxy.Port) + { + return unchecked((int)row.OwningPid); + } + + rowPtr = IntPtr.Add(rowPtr, rowSize); + } + } + finally + { + if (buffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(buffer); + } + } + + return 0; + } + + private static ProcessIdentity GetIdentity(int processId) + { + if (processId <= 0) + { + return ProcessIdentity.Unknown; + } + + try + { + using Process process = Process.GetProcessById(processId); + string path = null; + try + { + path = process.MainModule?.FileName; + } + catch + { + // Some protected/elevated processes deny MainModule to a normal user. + } + + string name; + try + { + name = process.ProcessName; + } + catch + { + name = null; + } + + return new ProcessIdentity(processId, path, name); + } + catch + { + return new ProcessIdentity(processId, null, null); + } + } + + private static uint ToIpv4UInt32(IPAddress address) + { + byte[] bytes = address.MapToIPv4().GetAddressBytes(); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + private static int DecodePort(uint encoded) + { + ushort networkOrder = unchecked((ushort)encoded); + return unchecked((ushort)IPAddress.NetworkToHostOrder(unchecked((short)networkOrder))); + } + + private enum TcpTableClass + { + TcpTableBasicListener, + TcpTableBasicConnections, + TcpTableBasicAll, + TcpTableOwnerPidListener, + TcpTableOwnerPidConnections, + TcpTableOwnerPidAll, + TcpTableOwnerModuleListener, + TcpTableOwnerModuleConnections, + TcpTableOwnerModuleAll, + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibTcpRowOwnerPid + { + public uint State; + public uint LocalAddr; + public uint LocalPort; + public uint RemoteAddr; + public uint RemotePort; + public uint OwningPid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MibTcp6RowOwnerPid + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] LocalAddr; + public uint LocalScopeId; + public uint LocalPort; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] RemoteAddr; + public uint RemoteScopeId; + public uint RemotePort; + public uint State; + public uint OwningPid; + } + + private static class NativeMethods + { + [DllImport("iphlpapi.dll", SetLastError = true)] + internal static extern uint GetExtendedTcpTable( + IntPtr tcpTable, + ref int size, + [MarshalAs(UnmanagedType.Bool)] bool order, + int addressFamily, + TcpTableClass tableClass, + uint reserved); + } + } + + public readonly record struct ProcessIdentity(int ProcessId, string ProcessPath, string ProcessName) + { + public static ProcessIdentity Unknown => new(0, null, null); + public bool IsCurrentProcess => ProcessId == Environment.ProcessId; + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/GameModeManager.cs b/Shadowsocks.Windows/Controller/Traffic/GameModeManager.cs new file mode 100644 index 000000000..698818b0b --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/GameModeManager.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Traffic +{ + /// + /// Coordinates desired capture mode with automatic Game Mode. A configured + /// application entering the process list temporarily disables the WinDivert + /// capture child and driver; Admin Mode is restored automatically after it exits. + /// + internal sealed class GameModeManager : IAsyncDisposable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2); + private readonly AdminCaptureManager _adminCapture; + private readonly SemaphoreSlim _gate = new(1, 1); + private readonly Timer _timer; + + private Configuration _configuration; + private int[] _excludedProcessIds = []; + private DnsCaptureRuntimeState _dnsRuntime = DnsCaptureRuntimeState.Unavailable; + private string[] _runningGameApplications = []; + private bool _automaticGameMode; + private bool _disposed; + + public GameModeManager(AdminCaptureManager adminCapture) + { + ArgumentNullException.ThrowIfNull(adminCapture); + _adminCapture = adminCapture; + _adminCapture.StatusChanged += AdminCapture_StatusChanged; + _timer = new Timer(static state => ((GameModeManager)state).QueuePoll(), this, Timeout.Infinite, Timeout.Infinite); + } + + public TrafficRuntimeMode RuntimeMode + { + get + { + if (_configuration?.trafficCaptureMode != TrafficCaptureMode.Admin) + { + return TrafficRuntimeMode.User; + } + + if (_automaticGameMode || _adminCapture.IsGameMode) + { + return TrafficRuntimeMode.Game; + } + + return _adminCapture.IsCaptureActive ? TrafficRuntimeMode.Admin : TrafficRuntimeMode.User; + } + } + + public bool AutomaticGameMode => _automaticGameMode; + public IReadOnlyList RunningGameApplications => _runningGameApplications; + + public event EventHandler StatusChanged; + + private void AdminCapture_StatusChanged(object sender, EventArgs e) + { + StatusChanged?.Invoke(this, EventArgs.Empty); + } + + public async Task ApplyConfigurationAsync( + Configuration configuration, + IEnumerable excludedProcessIds, + DnsCaptureRuntimeState dnsRuntime, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(configuration); + + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + _configuration = configuration; + _excludedProcessIds = (excludedProcessIds ?? []).Where(pid => pid > 0).Distinct().ToArray(); + _dnsRuntime = dnsRuntime; + + _runningGameApplications = configuration.trafficCaptureMode == TrafficCaptureMode.Admin + ? FindRunningConfiguredGames(configuration.gameModeApplications) + : []; + _automaticGameMode = _runningGameApplications.Length > 0; + + await ApplyStateLockedAsync(cancellationToken).ConfigureAwait(false); + if (configuration.trafficCaptureMode == TrafficCaptureMode.Admin) + { + _timer.Change(PollInterval, PollInterval); + } + else + { + _timer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + finally + { + _gate.Release(); + } + } + + public async Task UpdateDnsRuntimeAsync( + DnsCaptureRuntimeState dnsRuntime, + CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + _dnsRuntime = dnsRuntime; + } + finally + { + _gate.Release(); + } + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_disposed) + { + return; + } + + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _automaticGameMode = false; + _runningGameApplications = []; + await _adminCapture.StopAsync(cancellationToken).ConfigureAwait(false); + StatusChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _gate.Release(); + } + } + + private void QueuePoll() + { + if (_disposed || _configuration?.trafficCaptureMode != TrafficCaptureMode.Admin) + { + return; + } + + _ = PollAsync(); + } + + private async Task PollAsync() + { + if (!await _gate.WaitAsync(0).ConfigureAwait(false)) + { + return; + } + + try + { + if (_disposed || _configuration?.trafficCaptureMode != TrafficCaptureMode.Admin) + { + return; + } + + string[] running = FindRunningConfiguredGames(_configuration.gameModeApplications); + bool detected = running.Length > 0; + bool processSetChanged = !_runningGameApplications.SequenceEqual(running, StringComparer.OrdinalIgnoreCase); + if (detected == _automaticGameMode && !processSetChanged) + { + return; + } + + _runningGameApplications = running; + bool modeChanged = detected != _automaticGameMode; + _automaticGameMode = detected; + + if (modeChanged) + { + await ApplyStateLockedAsync(CancellationToken.None).ConfigureAwait(false); + } + else + { + StatusChanged?.Invoke(this, EventArgs.Empty); + } + } + catch (Exception exception) + { + Logger.Warn(exception, "Unable to update automatic Game Mode state."); + } + finally + { + _gate.Release(); + } + } + + private async Task ApplyStateLockedAsync(CancellationToken cancellationToken) + { + if (_configuration is null || _configuration.trafficCaptureMode != TrafficCaptureMode.Admin) + { + await _adminCapture.StopAsync(cancellationToken).ConfigureAwait(false); + StatusChanged?.Invoke(this, EventArgs.Empty); + return; + } + + if (_automaticGameMode) + { + // Keep the elevated broker's pending configuration current while the + // capture child is paused. Do not create a broker/UAC prompt merely + // because DNSCrypt restarted while a game was already running. + if (_adminCapture.IsGameMode && _adminCapture.IsBrokerRunning) + { + await _adminCapture.StartOrUpdateAsync( + _configuration, + _excludedProcessIds, + _dnsRuntime, + cancellationToken).ConfigureAwait(false); + } + else + { + await _adminCapture.EnterGameModeAsync(cancellationToken).ConfigureAwait(false); + } + StatusChanged?.Invoke(this, EventArgs.Empty); + return; + } + + if (_adminCapture.IsGameMode) + { + // Update the broker's stored request before resuming capture so a DNSCrypt + // restart that happened during Game Mode cannot restore a stale PID/port. + await _adminCapture.StartOrUpdateAsync( + _configuration, + _excludedProcessIds, + _dnsRuntime, + cancellationToken).ConfigureAwait(false); + await _adminCapture.ExitGameModeAsync(cancellationToken).ConfigureAwait(false); + StatusChanged?.Invoke(this, EventArgs.Empty); + return; + } + + await _adminCapture.StartOrUpdateAsync( + _configuration, + _excludedProcessIds, + _dnsRuntime, + cancellationToken).ConfigureAwait(false); + + StatusChanged?.Invoke(this, EventArgs.Empty); + } + + private static string[] FindRunningConfiguredGames(List patterns) + { + if (patterns is null || patterns.Count == 0) + { + return []; + } + + Process[] processes; + try + { + processes = Process.GetProcesses(); + } + catch + { + return []; + } + + var matches = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + foreach (Process process in processes) + { + string name = null; + string path = null; + try + { + name = process.ProcessName; + } + catch + { + } + + try + { + path = process.MainModule?.FileName; + } + catch + { + } + + foreach (string pattern in patterns) + { + if (!ApplicationPatternMatcher.Matches(pattern, path, name)) + { + continue; + } + + string displayName = !string.IsNullOrWhiteSpace(path) + ? System.IO.Path.GetFileName(path) + : string.IsNullOrWhiteSpace(name) + ? pattern + : name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe"; + matches.Add(displayName); + } + } + } + finally + { + foreach (Process process in processes) + { + try + { + process.Dispose(); + } + catch + { + } + } + } + + return matches.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray(); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + await StopAsync().ConfigureAwait(false); + _disposed = true; + _timer.Dispose(); + _adminCapture.StatusChanged -= AdminCapture_StatusChanged; + await _adminCapture.DisposeAsync().ConfigureAwait(false); + _gate.Dispose(); + } + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/NetworkServiceRuntime.cs b/Shadowsocks.Windows/Controller/Traffic/NetworkServiceRuntime.cs new file mode 100644 index 000000000..e50b0b1ca --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/NetworkServiceRuntime.cs @@ -0,0 +1,337 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using NLog; +using Shadowsocks.Core; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Traffic +{ + internal static class NetworkServiceRuntime + { + internal const string EmbeddedResourceName = "Shadowsocks.WinUI.Embedded.Shadowsocks.NetworkService.exe"; + private const string ExtractionMutexName = @"Local\Shadowsocks.Reborn.NetworkService.Extraction"; + private static readonly TimeSpan StaleRuntimeAge = TimeSpan.FromHours(24); + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + internal sealed class HelperLease : IDisposable + { + private FileStream guardStream; + private readonly string runDirectory; + private readonly bool deleteOnDispose; + private bool disposed; + + internal HelperLease( + string path, + string expectedSha256, + FileStream guardStream, + string runDirectory, + bool deleteOnDispose) + { + ArgumentNullException.ThrowIfNull(path); + Path = path; + ExpectedSha256 = expectedSha256 ?? string.Empty; + this.guardStream = guardStream; + this.runDirectory = runDirectory; + this.deleteOnDispose = deleteOnDispose; + } + + public string Path { get; } + public string ExpectedSha256 { get; } + + public void Dispose() + { + if (disposed) + return; + disposed = true; + + try + { + guardStream?.Dispose(); + } + finally + { + guardStream = null; + if (deleteOnDispose) + { + TryDeleteDirectory(runDirectory); + TryPruneEmptyParents(System.IO.Path.GetDirectoryName(runDirectory), AppStoragePaths.TempNetworkServiceRoot); + } + } + } + } + + /// + /// Materializes the embedded elevated helper on demand. This method is called only by + /// AdminCaptureManager while entering Admin Mode; User Mode never extracts the helper. + /// The returned lease keeps a read-only file handle open for the entire broker lifetime, + /// preventing another process from replacing/deleting the validated executable before or + /// while it is used for elevation. + /// + public static HelperLease AcquireHelper() + { + Assembly entryAssembly = Assembly.GetEntryAssembly(); + using Stream resource = entryAssembly?.GetManifestResourceStream(EmbeddedResourceName); + if (resource is null) + return AcquireDevelopmentHelper(); + + using MemoryStream buffer = new(); + resource.CopyTo(buffer); + byte[] helperBytes = buffer.ToArray(); + if (helperBytes.Length == 0) + throw new InvalidDataException("Embedded Shadowsocks.NetworkService.exe is empty."); + + string hash = Convert.ToHexString(SHA256.HashData(helperBytes)); + string version = SanitizePathComponent(ApplicationInfo.Version); + string versionDirectory = System.IO.Path.Combine(AppStoragePaths.TempNetworkServiceRoot, version); + string hashDirectory = System.IO.Path.Combine(versionDirectory, hash[..16]); + + using Mutex mutex = new(initiallyOwned: false, ExtractionMutexName); + bool lockTaken = false; + try + { + try + { + lockTaken = mutex.WaitOne(TimeSpan.FromSeconds(30)); + } + catch (AbandonedMutexException) + { + lockTaken = true; + } + + if (!lockTaken) + throw new TimeoutException("Timed out waiting for the NetworkService extraction lock."); + + Directory.CreateDirectory(hashDirectory); + CleanupStaleHelpers(keepDirectory: hashDirectory); + + // Keep one stable helper path per application version + embedded SHA-256. + // Windows Defender Firewall keys application consent to the executable path; + // the previous PID/GUID directory made the same signed/hashed helper look like + // a brand-new application on every Admin Mode start and repeatedly triggered + // the public/private network access prompt. The SHA directory already gives us + // immutable versioning, so another per-run directory is unnecessary. + string runDirectory = hashDirectory; + string targetPath = System.IO.Path.Combine(runDirectory, "Shadowsocks.NetworkService.exe"); + string temporaryPath = targetPath + ".new"; + + try + { + if (!IsFileHashValid(targetPath, hash)) + { + TryDeleteFile(targetPath); + TryDeleteFile(temporaryPath); + using (FileStream output = new( + temporaryPath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + bufferSize: 1024 * 64, + FileOptions.WriteThrough)) + { + output.Write(helperBytes, 0, helperBytes.Length); + output.Flush(flushToDisk: true); + } + + if (!IsFileHashValid(temporaryPath, hash)) + throw new InvalidDataException("Extracted NetworkService staging hash validation failed."); + + File.Move(temporaryPath, targetPath, overwrite: false); + } + + if (!IsFileHashValid(targetPath, hash)) + throw new InvalidDataException("Extracted NetworkService hash validation failed."); + + // Keep the file guarded for the whole elevated broker lifetime. FileShare.Read + // lets Windows load the image while denying replacement/write/delete access. + FileStream guard = new(targetPath, FileMode.Open, FileAccess.Read, FileShare.Read); + Logger.Info( + "NetworkService helper ready at stable path: {0} (version {1}, SHA256 {2})", + targetPath, + ApplicationInfo.Version, + hash); + return new HelperLease(targetPath, hash, guard, runDirectory, deleteOnDispose: false); + } + catch + { + TryDeleteFile(temporaryPath); + throw; + } + } + finally + { + if (lockTaken) + mutex.ReleaseMutex(); + } + } + + private static HelperLease AcquireDevelopmentHelper() + { + string[] candidates = + [ + System.IO.Path.Combine(AppContext.BaseDirectory, "Shadowsocks.NetworkService.exe"), + System.IO.Path.Combine(RuntimeEnvironment.WorkingDirectory, "Shadowsocks.NetworkService.exe"), + ]; + string path = candidates.FirstOrDefault(File.Exists); + if (path is null) + { + throw new FileNotFoundException( + "Shadowsocks.NetworkService.exe is neither embedded nor available in the development output.", + candidates[0]); + } + + Logger.Debug("Using development NetworkService helper: {0}", path); + return new HelperLease(path, string.Empty, guardStream: null, runDirectory: null, deleteOnDispose: false); + } + + private static bool IsFileHashValid(string path, string expectedHash) + { + if (!File.Exists(path)) + return false; + try + { + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + string actual = Convert.ToHexString(SHA256.HashData(stream)); + return string.Equals(actual, expectedHash, StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + internal static void CleanupStaleRuntime() + { + CleanupStaleHelpers(keepDirectory: null); + } + + private static void CleanupStaleHelpers(string keepDirectory) + { + try + { + string root = AppStoragePaths.TempNetworkServiceRoot; + if (!Directory.Exists(root)) + return; + + DateTime cutoffUtc = DateTime.UtcNow - StaleRuntimeAge; + foreach (string versionDirectory in Directory.EnumerateDirectories(root)) + { + foreach (string hashDirectory in SafeEnumerateDirectories(versionDirectory)) + { + if (!string.IsNullOrWhiteSpace(keepDirectory) + && string.Equals(hashDirectory, keepDirectory, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string stableHelper = System.IO.Path.Combine(hashDirectory, "Shadowsocks.NetworkService.exe"); + if (File.Exists(stableHelper)) + { + try + { + DateTime lastWriteUtc = File.GetLastWriteTimeUtc(stableHelper); + if (lastWriteUtc <= cutoffUtc) + Directory.Delete(hashDirectory, recursive: true); + } + catch + { + // The helper may still be guarded by a live broker. + } + } + + TryDeleteEmptyDirectory(hashDirectory); + } + + TryDeleteEmptyDirectory(versionDirectory); + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to clean all stale NetworkService helper directories."); + } + } + + private static string[] SafeEnumerateDirectories(string path) + { + try + { + return Directory.Exists(path) ? Directory.GetDirectories(path) : []; + } + catch + { + return []; + } + } + + private static string SanitizePathComponent(string value) + { + string result = value ?? "unknown"; + foreach (char invalid in System.IO.Path.GetInvalidFileNameChars()) + result = result.Replace(invalid, '_'); + return string.IsNullOrWhiteSpace(result) ? "unknown" : result.Replace('.', '_'); + } + + private static void TryDeleteFile(string path) + { + try + { + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + File.Delete(path); + } + catch + { + } + } + + private static void TryDeleteDirectory(string path) + { + try + { + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + catch + { + } + } + + private static void TryDeleteEmptyDirectory(string path) + { + try + { + if (!string.IsNullOrWhiteSpace(path) + && Directory.Exists(path) + && !Directory.EnumerateFileSystemEntries(path).Any()) + { + Directory.Delete(path); + } + } + catch + { + } + } + + private static void TryPruneEmptyParents(string directory, string stopAt) + { + try + { + string current = directory; + while (!string.IsNullOrWhiteSpace(current) + && current.StartsWith(stopAt, StringComparison.OrdinalIgnoreCase) + && !string.Equals(current, stopAt, StringComparison.OrdinalIgnoreCase)) + { + if (!Directory.Exists(current) || Directory.EnumerateFileSystemEntries(current).Any()) + break; + Directory.Delete(current); + current = System.IO.Path.GetDirectoryName(current); + } + } + catch + { + } + } + } +} diff --git a/Shadowsocks.Windows/Controller/Traffic/WinDivertInstaller.cs b/Shadowsocks.Windows/Controller/Traffic/WinDivertInstaller.cs new file mode 100644 index 000000000..d4289050f --- /dev/null +++ b/Shadowsocks.Windows/Controller/Traffic/WinDivertInstaller.cs @@ -0,0 +1,333 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Shadowsocks.Controller.Service; +using Shadowsocks.Model; +using Shadowsocks.Core.Storage; + +namespace Shadowsocks.Controller.Traffic +{ + /// + /// Downloads the official WinDivert runtime only when Admin Mode is requested. + /// The driver is not embedded in Shadowsocks and is not touched by User Mode. + /// + internal sealed class WinDivertInstaller : IDisposable + { + public const string Version = "2.2.2"; + public const string PackageUrl = "https://github.com/basil00/WinDivert/releases/download/v2.2.2/WinDivert-2.2.2-A.zip"; + public const string DllSha256 = "c1e060ee19444a259b2162f8af0f3fe8c4428a1c6f694dce20de194ac8d7d9a2"; + public const string DriverSha256 = "8da085332782708d8767bcace5327a6ec7283c17cfb85e40b03cd2323a90ddc2"; + + private const int MaxPackageBytes = 8 * 1024 * 1024; + private const long MaxRuntimeEntryBytes = 4 * 1024 * 1024; + private const ushort ImageFileMachineAmd64 = 0x8664; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly SemaphoreSlim _installLock = new(1, 1); + private bool _disposed; + + public async Task EnsureInstalledAsync(Configuration configuration, CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(configuration); + + string installDirectory = GetInstallDirectory(configuration); + string dllPath = Path.Combine(installDirectory, "WinDivert.dll"); + string driverPath = Path.Combine(installDirectory, "WinDivert64.sys"); + if (TryValidateInstalledRuntime(dllPath, driverPath)) + { + return installDirectory; + } + + await _installLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (TryValidateInstalledRuntime(dllPath, driverPath)) + { + return installDirectory; + } + + TryDeleteDirectory(installDirectory); + Directory.CreateDirectory(installDirectory); + try + { + byte[] package = await DownloadPackageAsync(configuration, cancellationToken).ConfigureAwait(false); + ExtractRuntime(package, installDirectory); + + if (!File.Exists(dllPath) || !File.Exists(driverPath)) + { + throw new InvalidDataException("WinDivert package did not contain the x64 runtime files."); + } + + ValidatePinnedRuntimeFile(dllPath, DllSha256); + ValidatePinnedRuntimeFile(driverPath, DriverSha256); + + string marker = Path.Combine(installDirectory, "source.txt"); + File.WriteAllText( + marker, + $"WinDivert {Version}{Environment.NewLine}" + + $"{PackageUrl}{Environment.NewLine}" + + $"WinDivert.dll SHA-256: {DllSha256}{Environment.NewLine}" + + $"WinDivert64.sys SHA-256: {DriverSha256}{Environment.NewLine}"); + return installDirectory; + } + catch + { + TryDeleteDirectory(installDirectory); + throw; + } + } + finally + { + _installLock.Release(); + } + } + + public static string GetInstallDirectory(Configuration configuration) + { + return Path.Combine(AppStoragePaths.WinDivertRuntimeRoot, Version); + } + + private static async Task DownloadPackageAsync(Configuration configuration, CancellationToken cancellationToken) + { + Exception proxiedFailure = null; + try + { + using HttpClient proxiedClient = LocalProxyHttpClient.Create(configuration); + return await DownloadBoundedAsync(proxiedClient, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + proxiedFailure = exception; + Logger.Warn(exception, "Unable to download WinDivert through the local Shadowsocks proxy; trying direct HTTPS."); + } + + try + { + using HttpClient directClient = new() + { + Timeout = TimeSpan.FromSeconds(30), + }; + directClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", configuration.userAgentString); + return await DownloadBoundedAsync(directClient, cancellationToken).ConfigureAwait(false); + } + catch (Exception directFailure) when (directFailure is not OperationCanceledException) + { + throw new HttpRequestException( + "Unable to download the official WinDivert package through Shadowsocks or directly.", + new AggregateException(proxiedFailure, directFailure)); + } + } + + internal static void ExtractRuntime(byte[] package, string installDirectory) + { + ExtractRuntime(package, installDirectory, DllSha256, DriverSha256); + } + + internal static void ExtractRuntime( + byte[] package, + string installDirectory, + string expectedDllSha256, + string expectedDriverSha256) + { + ArgumentNullException.ThrowIfNull(package); + if (package.Length == 0 || package.Length > MaxPackageBytes) + throw new InvalidDataException("WinDivert package size is invalid."); + + using MemoryStream packageStream = new(package, writable: false); + using ZipArchive archive = new(packageStream, ZipArchiveMode.Read, leaveOpen: false); + + string archiveRoot = $"WinDivert-{Version}-A/x64/"; + ExtractRequiredEntry( + archive, + archiveRoot + "WinDivert.dll", + Path.Combine(installDirectory, "WinDivert.dll"), + expectedDllSha256); + ExtractRequiredEntry( + archive, + archiveRoot + "WinDivert64.sys", + Path.Combine(installDirectory, "WinDivert64.sys"), + expectedDriverSha256); + } + + internal static void ValidateX64PortableExecutable(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + if (stream.Length < 0x40) + throw new InvalidDataException($"WinDivert runtime file '{Path.GetFileName(path)}' is not a valid PE image."); + + using var reader = new BinaryReader(stream); + if (reader.ReadUInt16() != 0x5A4D) + throw new InvalidDataException($"WinDivert runtime file '{Path.GetFileName(path)}' is missing the MZ header."); + + stream.Position = 0x3C; + int peOffset = reader.ReadInt32(); + if (peOffset < 0x40 || peOffset > stream.Length - 6) + throw new InvalidDataException($"WinDivert runtime file '{Path.GetFileName(path)}' has an invalid PE offset."); + + stream.Position = peOffset; + if (reader.ReadUInt32() != 0x00004550) + throw new InvalidDataException($"WinDivert runtime file '{Path.GetFileName(path)}' is missing the PE signature."); + if (reader.ReadUInt16() != ImageFileMachineAmd64) + throw new InvalidDataException($"WinDivert runtime file '{Path.GetFileName(path)}' is not an x64 PE image."); + } + + internal static void ValidatePinnedRuntimeFile(string path, string expectedSha256) + { + ValidateX64PortableExecutable(path); + byte[] expectedHash; + try + { + expectedHash = Convert.FromHexString(expectedSha256); + } + catch (FormatException exception) + { + throw new InvalidOperationException("The pinned WinDivert SHA-256 value is invalid.", exception); + } + + if (expectedHash.Length != 32) + throw new InvalidOperationException("The pinned WinDivert SHA-256 value must contain 32 bytes."); + + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + byte[] actualHash = SHA256.HashData(stream); + if (!CryptographicOperations.FixedTimeEquals(actualHash, expectedHash)) + { + throw new InvalidDataException( + $"WinDivert runtime file '{Path.GetFileName(path)}' did not match its pinned SHA-256 digest."); + } + } + + private static bool TryValidateInstalledRuntime(string dllPath, string driverPath) + { + if (!File.Exists(dllPath) || !File.Exists(driverPath)) + return false; + + try + { + ValidatePinnedRuntimeFile(dllPath, DllSha256); + ValidatePinnedRuntimeFile(driverPath, DriverSha256); + return true; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) + { + Logger.Warn(exception, "Cached WinDivert runtime is invalid and will be downloaded again."); + return false; + } + } + + private static async Task DownloadBoundedAsync(HttpClient client, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await client.GetAsync( + PackageUrl, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + long? contentLength = response.Content.Headers.ContentLength; + if (contentLength.HasValue && (contentLength.Value <= 0 || contentLength.Value > MaxPackageBytes)) + throw new InvalidDataException("WinDivert package Content-Length is invalid."); + + await using Stream input = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + int initialCapacity = contentLength.HasValue && contentLength.Value > 0 && contentLength.Value <= MaxPackageBytes + ? (int)contentLength.Value + : 0; + using var output = new MemoryStream(initialCapacity); + byte[] buffer = new byte[81920]; + int total = 0; + while (true) + { + int read = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + + total = checked(total + read); + if (total > MaxPackageBytes) + throw new InvalidDataException("WinDivert package exceeded the maximum allowed size."); + output.Write(buffer, 0, read); + } + + if (total == 0) + throw new InvalidDataException("WinDivert package was empty."); + return output.ToArray(); + } + + private static void ExtractRequiredEntry( + ZipArchive archive, + string expectedPath, + string destination, + string expectedSha256) + { + string normalizedExpected = expectedPath.Replace('\\', '/'); + ZipArchiveEntry[] matches = archive.Entries + .Where(candidate => string.Equals( + candidate.FullName.Replace('\\', '/'), + normalizedExpected, + StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (matches.Length != 1) + throw new InvalidDataException($"WinDivert package must contain exactly one '{normalizedExpected}' entry."); + + ZipArchiveEntry entry = matches[0]; + if (entry.Length <= 0 || entry.Length > MaxRuntimeEntryBytes) + throw new InvalidDataException($"WinDivert package entry '{normalizedExpected}' has an invalid size."); + + string temporary = destination + ".new"; + try + { + using (Stream input = entry.Open()) + using (FileStream output = new(temporary, FileMode.Create, FileAccess.Write, FileShare.None)) + { + input.CopyTo(output); + if (output.Length != entry.Length || output.Length > MaxRuntimeEntryBytes) + throw new InvalidDataException($"WinDivert package entry '{normalizedExpected}' extracted to an invalid size."); + output.Flush(flushToDisk: true); + } + + ValidatePinnedRuntimeFile(temporary, expectedSha256); + File.Move(temporary, destination, overwrite: true); + } + finally + { + try + { + File.Delete(temporary); + } + catch + { + } + } + } + + private static void TryDeleteDirectory(string path) + { + try + { + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + Logger.Warn(exception, "Unable to clear the invalid WinDivert runtime directory before reinstalling."); + } + } + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _installLock.Dispose(); + GC.SuppressFinalize(this); + } + + } +} diff --git a/Shadowsocks.Windows/FodyWeavers.xml b/Shadowsocks.Windows/FodyWeavers.xml new file mode 100644 index 000000000..4872b6b2c --- /dev/null +++ b/Shadowsocks.Windows/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Shadowsocks.Windows/Properties/AssemblyInfo.cs b/Shadowsocks.Windows/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..024bc0cfa --- /dev/null +++ b/Shadowsocks.Windows/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("shadowsocks-reborn")] +[assembly: InternalsVisibleTo("Shadowsocks")] +[assembly: InternalsVisibleTo("Shadowsocks.UnitTests")] diff --git a/Shadowsocks.Windows/Shadowsocks.Windows.csproj b/Shadowsocks.Windows/Shadowsocks.Windows.csproj new file mode 100644 index 000000000..5b281ecaf --- /dev/null +++ b/Shadowsocks.Windows/Shadowsocks.Windows.csproj @@ -0,0 +1,32 @@ + + + net10.0-windows10.0.19041.0 + Shadowsocks community + https://github.com/SlimRG/shadowsocks-reborn + git + Shadowsocks + Shadowsocks.Windows + true + x64 + x64 + + false + disable + disable + true + + + + + + + + + + + + + + diff --git a/Shadowsocks.Windows/Shell/NativeUserInteractionService.cs b/Shadowsocks.Windows/Shell/NativeUserInteractionService.cs new file mode 100644 index 000000000..1f5d486fb --- /dev/null +++ b/Shadowsocks.Windows/Shell/NativeUserInteractionService.cs @@ -0,0 +1,152 @@ +#nullable enable + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Shadowsocks.Core; + +namespace Shadowsocks.Windows.Shell; + +/// +/// Synchronous Windows user-interaction boundary for controller code. +/// Uses Win32 primitives so the controller can preserve its original synchronous +/// confirm/error semantics without reintroducing WinForms/WPF or blocking WinUI +/// ContentDialog asynchronous operations. +/// +public sealed partial class NativeUserInteractionService : IUserInteractionService +{ + private const uint MbOk = 0x00000000; + private const uint MbYesNo = 0x00000004; + private const uint MbIconError = 0x00000010; + private const uint MbIconQuestion = 0x00000020; + private const uint MbIconWarning = 0x00000030; + private const uint MbIconInformation = 0x00000040; + private const uint MbSetForeground = 0x00010000; + private const int IdYes = 6; + + private const uint CfUnicodeText = 13; + private const uint GmemMoveable = 0x0002; + private const uint GmemZeroInit = 0x0040; + + public nint OwnerWindowHandle { get; set; } + + public bool Confirm(string message, string? title) + => ShowMessage(message, title, MbYesNo | MbIconQuestion) == IdYes; + + public void ShowInfo(string message, string? title) + => _ = ShowMessage(message, title, MbOk | MbIconInformation); + + public void ShowWarning(string message, string? title) + => _ = ShowMessage(message, title, MbOk | MbIconWarning); + + public void ShowError(string message, string? title) + => _ = ShowMessage(message, title, MbOk | MbIconError); + + public void SetClipboardText(string text) + { + if (string.IsNullOrEmpty(text)) + { + return; + } + + byte[] bytes = Encoding.Unicode.GetBytes(text + "\0"); + nint memoryHandle = GlobalAlloc(GmemMoveable | GmemZeroInit, (nuint)bytes.Length); + if (memoryHandle == nint.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + bool clipboardOpened = false; + bool ownershipTransferred = false; + try + { + nint memory = GlobalLock(memoryHandle); + if (memory == nint.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + try + { + Marshal.Copy(bytes, 0, memory, bytes.Length); + } + finally + { + _ = GlobalUnlock(memoryHandle); + } + + for (int attempt = 0; attempt < 10 && !clipboardOpened; attempt++) + { + clipboardOpened = OpenClipboard(OwnerWindowHandle) != 0; + if (!clipboardOpened) + { + Thread.Sleep(10); + } + } + + if (!clipboardOpened) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (EmptyClipboard() == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (SetClipboardData(CfUnicodeText, memoryHandle) == nint.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + ownershipTransferred = true; + } + finally + { + if (clipboardOpened) + { + _ = CloseClipboard(); + } + + if (!ownershipTransferred) + { + _ = GlobalFree(memoryHandle); + } + } + } + + private int ShowMessage(string message, string? title, uint flags) + { + string caption = string.IsNullOrWhiteSpace(title) ? "shadowsocks-reborn" : title; + return MessageBox(OwnerWindowHandle, message ?? string.Empty, caption, flags | MbSetForeground); + } + + [LibraryImport("user32.dll", EntryPoint = "MessageBoxW", StringMarshalling = StringMarshalling.Utf16)] + private static partial int MessageBox(nint windowHandle, string text, string caption, uint type); + + [LibraryImport("user32.dll", EntryPoint = "OpenClipboard", SetLastError = true)] + private static partial int OpenClipboard(nint newOwner); + + [LibraryImport("user32.dll", EntryPoint = "CloseClipboard", SetLastError = true)] + private static partial int CloseClipboard(); + + [LibraryImport("user32.dll", EntryPoint = "EmptyClipboard", SetLastError = true)] + private static partial int EmptyClipboard(); + + [LibraryImport("user32.dll", EntryPoint = "SetClipboardData", SetLastError = true)] + private static partial nint SetClipboardData(uint format, nint memoryHandle); + + [LibraryImport("kernel32.dll", EntryPoint = "GlobalAlloc", SetLastError = true)] + private static partial nint GlobalAlloc(uint flags, nuint bytes); + + [LibraryImport("kernel32.dll", EntryPoint = "GlobalLock", SetLastError = true)] + private static partial nint GlobalLock(nint memoryHandle); + + [LibraryImport("kernel32.dll", EntryPoint = "GlobalUnlock", SetLastError = true)] + private static partial int GlobalUnlock(nint memoryHandle); + + [LibraryImport("kernel32.dll", EntryPoint = "GlobalFree", SetLastError = true)] + private static partial nint GlobalFree(nint memoryHandle); +} diff --git a/Shadowsocks.Windows/Shell/WindowsCommandLine.cs b/Shadowsocks.Windows/Shell/WindowsCommandLine.cs new file mode 100644 index 000000000..0ffe0135e --- /dev/null +++ b/Shadowsocks.Windows/Shell/WindowsCommandLine.cs @@ -0,0 +1,49 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Shadowsocks.Windows.Shell; + +public static partial class WindowsCommandLine +{ + public static string[] ParseArguments(string? arguments) + { + if (string.IsNullOrWhiteSpace(arguments)) + { + return Array.Empty(); + } + + // CommandLineToArgvW expects a complete command line. Add a synthetic executable + // so the returned argv semantics match Environment.GetCommandLineArgs(). + string commandLine = $"shadowsocks-reborn.exe {arguments}"; + nint argumentVector = CommandLineToArgv(commandLine, out int argumentCount); + if (argumentVector == nint.Zero) + { + return Array.Empty(); + } + + try + { + var result = new List(Math.Max(0, argumentCount - 1)); + for (int index = 1; index < argumentCount; index++) + { + nint argumentPointer = Marshal.ReadIntPtr(argumentVector, index * IntPtr.Size); + result.Add(Marshal.PtrToStringUni(argumentPointer) ?? string.Empty); + } + + return result.ToArray(); + } + finally + { + _ = LocalFree(argumentVector); + } + } + + [LibraryImport("shell32.dll", EntryPoint = "CommandLineToArgvW", StringMarshalling = StringMarshalling.Utf16)] + private static partial nint CommandLineToArgv(string commandLine, out int argumentCount); + + [LibraryImport("kernel32.dll", EntryPoint = "LocalFree")] + private static partial nint LocalFree(nint memory); +} diff --git a/Shadowsocks.Windows/Storage/WindowsStorageBootstrapper.cs b/Shadowsocks.Windows/Storage/WindowsStorageBootstrapper.cs new file mode 100644 index 000000000..37859a840 --- /dev/null +++ b/Shadowsocks.Windows/Storage/WindowsStorageBootstrapper.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using NLog; +using Shadowsocks.Core.Storage; +using Shadowsocks.Model; + +namespace Shadowsocks.Windows.Storage +{ + public static class WindowsStorageBootstrapper + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static bool initialized; + + public static void Initialize() + { + if (initialized) + { + return; + } + + AppStoragePaths.EnsureStorageDirectories(); + Configuration.ConfigureSettingsStore( + new JsonFileSettingsStore(AppStoragePaths.SettingsFile, AppStoragePaths.SettingsBackupFile)); + + CleanupStaleTempData(); + initialized = true; + } + + private static void CleanupStaleTempData() + { + AppStoragePaths.CleanupStaleCleanSessions(TimeSpan.FromDays(2)); + PruneTempChildren(AppStoragePaths.TempStartupLogsRoot, TimeSpan.FromDays(2)); + PruneTempChildren(AppStoragePaths.TempUpdatesRoot, TimeSpan.FromDays(7)); + PruneTempChildren(AppStoragePaths.TempWorkingRoot, TimeSpan.FromDays(7)); + Shadowsocks.Controller.Traffic.NetworkServiceRuntime.CleanupStaleRuntime(); + } + + private static void PruneTempChildren(string root, TimeSpan retention) + { + if (!Directory.Exists(root)) + { + return; + } + + DateTime cutoffUtc = DateTime.UtcNow - retention; + foreach (string entry in Directory.EnumerateFileSystemEntries(root)) + { + try + { + DateTime lastWriteUtc = File.Exists(entry) + ? File.GetLastWriteTimeUtc(entry) + : Directory.GetLastWriteTimeUtc(entry); + if (lastWriteUtc > cutoffUtc) + { + continue; + } + + if (File.Exists(entry)) + { + File.Delete(entry); + } + else if (Directory.Exists(entry)) + { + Directory.Delete(entry, recursive: true); + } + } + catch (Exception exception) + { + Logger.Debug(exception, "Unable to prune stale temp entry {0}.", entry); + } + } + } + } +} diff --git a/Shadowsocks.Windows/Util/ProcessManagement/Job.cs b/Shadowsocks.Windows/Util/ProcessManagement/Job.cs new file mode 100644 index 000000000..22efb8e16 --- /dev/null +++ b/Shadowsocks.Windows/Util/ProcessManagement/Job.cs @@ -0,0 +1,153 @@ +#nullable enable +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using NLog; + +namespace Shadowsocks.Util.ProcessManagement +{ + /// + /// Windows Job Object configured with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. + /// All assigned processes are terminated automatically when the job handle is closed. + /// + public sealed class Job : IDisposable + { + private const uint JobObjectLimitKillOnJobClose = 0x00002000; + private const int JobObjectExtendedLimitInformationClass = 9; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly SafeFileHandle _handle; + private bool _disposed; + + public Job() + { + nint rawHandle = CreateJobObjectW(nint.Zero, null); + if (rawHandle == nint.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to create process job object."); + } + + _handle = new SafeFileHandle(rawHandle, ownsHandle: true); + try + { + var information = new JobObjectExtendedLimitInformation + { + BasicLimitInformation = new JobObjectBasicLimitInformation + { + LimitFlags = JobObjectLimitKillOnJobClose, + }, + }; + + int size = Marshal.SizeOf(); + if (!SetInformationJobObject( + _handle, + JobObjectExtendedLimitInformationClass, + ref information, + (uint)size)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to configure process job object."); + } + } + catch + { + _handle.Dispose(); + throw; + } + } + + public bool AddProcess(IntPtr processHandle) + { + ObjectDisposedException.ThrowIf(_disposed, this); + bool success = AssignProcessToJobObject(_handle, processHandle); + if (!success) + { + Logger.Error("AssignProcessToJobObject failed. GetLastError={Error}", Marshal.GetLastWin32Error()); + } + + return success; + } + + public bool AddProcess(int processId) + { + using Process process = Process.GetProcessById(processId); + return AddProcess(process.Handle); + } + + public void AddProcessOrThrow(Process process, string processDescription = "child process") + { + ArgumentNullException.ThrowIfNull(process); + ObjectDisposedException.ThrowIf(_disposed, this); + + if (!AssignProcessToJobObject(_handle, process.Handle)) + { + throw new Win32Exception( + Marshal.GetLastWin32Error(), + $"Unable to assign {processDescription} to its process job object."); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _handle.Dispose(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct JobObjectBasicLimitInformation + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IoCounters + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JobObjectExtendedLimitInformation + { + public JobObjectBasicLimitInformation BasicLimitInformation; + public IoCounters IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetInformationJobObject( + SafeFileHandle hJob, + int jobObjectInformationClass, + ref JobObjectExtendedLimitInformation lpJobObjectInformation, + uint cbJobObjectInformationLength); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AssignProcessToJobObject(SafeFileHandle hJob, nint hProcess); + } +} diff --git a/Shadowsocks.Windows/Util/SystemProxy/Ras.cs b/Shadowsocks.Windows/Util/SystemProxy/Ras.cs new file mode 100644 index 000000000..f72cb42d6 --- /dev/null +++ b/Shadowsocks.Windows/Util/SystemProxy/Ras.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Shadowsocks.Util.SystemProxy; + +internal static partial class Ras +{ + private const int MaxEntryName = 256; + private const int MaxPath = 260; + private const uint Success = 0; + private const uint ErrorBufferTooSmall = 603; + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct RasEntryName + { + public int Size; + public fixed char EntryName[MaxEntryName + 1]; + public int Flags; + public fixed char PhonebookPath[MaxPath + 1]; + } + + [LibraryImport("rasapi32.dll", EntryPoint = "RasEnumEntriesW")] + private static unsafe partial uint RasEnumEntries( + char* reserved, + char* phonebook, + RasEntryName* entries, + ref int bufferSize, + out int entryCount); + + public static unsafe string[] GetAllConnections() + { + var bufferSize = 0; + uint result = RasEnumEntries(null, null, null, ref bufferSize, out int entryCount); + if (result == Success && entryCount == 0) + { + return []; + } + + if (result != ErrorBufferTooSmall) + { + throw new Win32Exception((int)result); + } + + var entries = new RasEntryName[entryCount]; + var entrySize = sizeof(RasEntryName); + for (var i = 0; i < entries.Length; i++) + { + entries[i].Size = entrySize; + } + + fixed (RasEntryName* entriesPointer = entries) + { + result = RasEnumEntries(null, null, entriesPointer, ref bufferSize, out entryCount); + if (result != Success) + { + throw new Win32Exception((int)result); + } + + var names = new List(entryCount); + for (var i = 0; i < entryCount && i < entries.Length; i++) + { + var entryName = new string(entriesPointer[i].EntryName); + if (!string.IsNullOrWhiteSpace(entryName)) + { + names.Add(entryName); + } + } + + return [.. names]; + } + } +} diff --git a/Shadowsocks.Windows/Util/SystemProxy/WinINet.cs b/Shadowsocks.Windows/Util/SystemProxy/WinINet.cs new file mode 100644 index 000000000..14ec7fb87 --- /dev/null +++ b/Shadowsocks.Windows/Util/SystemProxy/WinINet.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using NLog; + +namespace Shadowsocks.Util.SystemProxy +{ + [Flags] + internal enum InternetPerConnectionFlags + { + Direct = 0x01, + Proxy = 0x02, + AutoProxyUrl = 0x04, + AutoDetect = 0x08, + } + + internal sealed class WinINetSetting + { + public InternetPerConnectionFlags Flags { get; init; } = InternetPerConnectionFlags.Direct; + public string ProxyServer { get; init; } = string.Empty; + public string ProxyBypass { get; init; } = string.Empty; + public string AutoConfigUrl { get; init; } = string.Empty; + } + + internal static class WinINet + { + private const int InternetOptionRefresh = 37; + private const int InternetOptionPerConnectionOption = 75; + private const int InternetOptionProxySettingsChanged = 95; + + private const int InternetPerConnFlags = 1; + private const int InternetPerConnProxyServer = 2; + private const int InternetPerConnProxyBypass = 3; + private const int InternetPerConnAutoConfigUrl = 4; + private const int ErrorFileNotFound = 2; + private const int ErrorWinHttpAutoProxyServiceError = 12178; + private const string InternetSettingsRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings"; + + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + private static readonly string[] LanBypass = + [ + "", + "localhost", + "127.*", + "10.*", + "172.16.*", + "172.17.*", + "172.18.*", + "172.19.*", + "172.20.*", + "172.21.*", + "172.22.*", + "172.23.*", + "172.24.*", + "172.25.*", + "172.26.*", + "172.27.*", + "172.28.*", + "172.29.*", + "172.30.*", + "172.31.*", + "192.168.*", + ]; + + private static WinINetSetting initialSetting; + private static bool queryFallbackLogged; + + public static bool Operational { get; private set; } = true; + + static WinINet() + { + try + { + initialSetting = Query(); + } + catch (DllNotFoundException ex) + { + Operational = false; + initialSetting = new(); + logger.Warn(ex, "Windows proxy APIs are unavailable; system proxy integration is disabled."); + } + catch (EntryPointNotFoundException ex) + { + Operational = false; + initialSetting = new(); + logger.Warn(ex, "Windows proxy APIs are unavailable; system proxy integration is disabled."); + } + catch (Win32Exception ex) + { + // Reading the previous state is only needed to restore it on exit. Do not + // disable proxy writes merely because the initial snapshot could not be read. + initialSetting = new(); + logger.Warn(ex, "Unable to snapshot the initial Windows proxy state; Direct will be restored on exit."); + } + } + + public static void ProxyGlobal(string server) + { + string bypass = BuildGlobalBypassList(initialSetting.ProxyBypass); + ApplyAndVerify( + [ + SetOption.Flags(InternetPerConnectionFlags.Direct | InternetPerConnectionFlags.Proxy), + SetOption.String(InternetPerConnProxyServer, server), + SetOption.String(InternetPerConnProxyBypass, bypass), + SetOption.String(InternetPerConnAutoConfigUrl, string.Empty), + ], + setting => + setting.Flags.HasFlag(InternetPerConnectionFlags.Proxy) && + string.Equals(setting.ProxyServer, server, StringComparison.OrdinalIgnoreCase), + "global proxy"); + } + + public static void ProxyPAC(string url) + { + ApplyAndVerify( + [ + SetOption.Flags(InternetPerConnectionFlags.Direct | InternetPerConnectionFlags.AutoProxyUrl), + SetOption.String(InternetPerConnProxyServer, string.Empty), + SetOption.String(InternetPerConnAutoConfigUrl, url), + ], + setting => + setting.Flags.HasFlag(InternetPerConnectionFlags.AutoProxyUrl) && + string.Equals(setting.AutoConfigUrl, url, StringComparison.OrdinalIgnoreCase), + "PAC proxy"); + } + + public static void Restore() + { + Set(initialSetting); + } + + public static void Reset() + { + Set(new()); + } + + public static WinINetSetting Query() + { + if (!Operational) + { + return new(); + } + + WinHttpCurrentUserIeProxyConfig native = default; + if (!WinHttpGetIEProxyConfigForCurrentUser(ref native)) + { + int error = Marshal.GetLastWin32Error(); + if (error != ErrorFileNotFound && + error != ErrorWinHttpAutoProxyServiceError && + !queryFallbackLogged) + { + queryFallbackLogged = true; + logger.Warn(new Win32Exception(error), + "Unable to read the active proxy state through WinHTTP; using the current-user Internet Settings registry as a fallback."); + } + + return QueryRegistryFallback(); + } + + try + { + string proxyServer = PtrToString(native.Proxy); + string proxyBypass = PtrToString(native.ProxyBypass); + string autoConfigUrl = PtrToString(native.AutoConfigUrl); + + InternetPerConnectionFlags flags = InternetPerConnectionFlags.Direct; + if (native.AutoDetect) + { + flags |= InternetPerConnectionFlags.AutoDetect; + } + if (!string.IsNullOrWhiteSpace(proxyServer)) + { + flags |= InternetPerConnectionFlags.Proxy; + } + if (!string.IsNullOrWhiteSpace(autoConfigUrl)) + { + flags |= InternetPerConnectionFlags.AutoProxyUrl; + } + + return new() + { + Flags = flags, + ProxyServer = proxyServer, + ProxyBypass = proxyBypass, + AutoConfigUrl = autoConfigUrl, + }; + } + finally + { + FreeGlobalString(native.AutoConfigUrl); + FreeGlobalString(native.Proxy); + FreeGlobalString(native.ProxyBypass); + } + } + + private static void Set(WinINetSetting setting) + { + SetOption[] options = + [ + SetOption.Flags(setting.Flags), + SetOption.String(InternetPerConnProxyServer, setting.ProxyServer), + SetOption.String(InternetPerConnProxyBypass, setting.ProxyBypass), + SetOption.String(InternetPerConnAutoConfigUrl, setting.AutoConfigUrl), + ]; + + try + { + Apply(options); + } + finally + { + DisposeOptions(options); + } + } + + private static void ApplyAndVerify(SetOption[] options, Func verifier, string description) + { + try + { + Apply(options); + if (verifier(Query())) + { + return; + } + + // A second direct WinINet notification is cheap and handles Windows races where + // another startup component updates Internet Settings at the same moment. + logger.Warn("WinINet did not retain the requested {0} state; applying it once more.", description); + Apply(options); + if (!verifier(Query())) + { + throw new InvalidOperationException($"Windows did not retain the requested {description} state."); + } + } + finally + { + DisposeOptions(options); + } + } + + private static void Apply(SetOption[] options) + { + if (!Operational) + { + throw new InvalidOperationException("WinINet system proxy integration is unavailable."); + } + + // LAN is the primary Windows proxy scope. A broken or unavailable RAS + // phonebook must not prevent the LAN proxy from being applied. + ApplyToConnection(options, null); + + string[] connections; + try + { + connections = Ras.GetAllConnections(); + } + catch (Win32Exception ex) + { + logger.Warn(ex, "Unable to enumerate RAS connections; LAN proxy settings were still applied."); + return; + } + + foreach (string connection in connections) + { + try + { + ApplyToConnection(options, connection); + } + catch (Win32Exception ex) + { + logger.Warn(ex, $"Unable to update WinINet proxy settings for RAS connection '{connection}'."); + } + } + } + + private static void DisposeOptions(SetOption[] options) + { + foreach (SetOption option in options) + { + option.Dispose(); + } + } + + private static void ApplyToConnection(SetOption[] options, string connectionName) + { + int optionSize = Marshal.SizeOf(); + IntPtr optionBuffer = Marshal.AllocCoTaskMem(optionSize * options.Length); + IntPtr connection = IntPtr.Zero; + IntPtr listBuffer = IntPtr.Zero; + try + { + IntPtr current = optionBuffer; + foreach (SetOption option in options) + { + Marshal.StructureToPtr(option.Native, current, false); + current += optionSize; + } + + if (!string.IsNullOrEmpty(connectionName)) + { + connection = Marshal.StringToHGlobalUni(connectionName); + } + + InternetPerConnectionOptionList list = new() + { + Size = Marshal.SizeOf(), + Connection = connection, + OptionCount = options.Length, + OptionError = 0, + Options = optionBuffer, + }; + + listBuffer = Marshal.AllocCoTaskMem(list.Size); + Marshal.StructureToPtr(list, listBuffer, false); + + if (!InternetSetOption(IntPtr.Zero, InternetOptionPerConnectionOption, listBuffer, list.Size)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + NotifyProxySettingsChanged(); + } + finally + { + if (listBuffer != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(listBuffer); + } + if (connection != IntPtr.Zero) + { + Marshal.FreeHGlobal(connection); + } + Marshal.FreeCoTaskMem(optionBuffer); + } + } + + private static void NotifyProxySettingsChanged() + { + if (!InternetSetOption(IntPtr.Zero, InternetOptionProxySettingsChanged, IntPtr.Zero, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + if (!InternetSetOption(IntPtr.Zero, InternetOptionRefresh, IntPtr.Zero, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + private static string BuildGlobalBypassList(string original) + { + HashSet entries = new(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(original)) + { + foreach (string item in original.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + entries.Add(item); + } + } + foreach (string item in LanBypass) + { + entries.Add(item); + } + return string.Join(";", entries); + } + + private static WinINetSetting QueryRegistryFallback() + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey(InternetSettingsRegistryPath); + if (key == null) + { + return new(); + } + + string proxyServer = key.GetValue("ProxyServer") as string ?? string.Empty; + string proxyBypass = key.GetValue("ProxyOverride") as string ?? string.Empty; + string autoConfigUrl = key.GetValue("AutoConfigURL") as string ?? string.Empty; + bool proxyEnabled = key.GetValue("ProxyEnable") is int proxyEnable && proxyEnable != 0; + + InternetPerConnectionFlags flags = InternetPerConnectionFlags.Direct; + if (proxyEnabled && !string.IsNullOrWhiteSpace(proxyServer)) + { + flags |= InternetPerConnectionFlags.Proxy; + } + if (!string.IsNullOrWhiteSpace(autoConfigUrl)) + { + flags |= InternetPerConnectionFlags.AutoProxyUrl; + } + + return new() + { + Flags = flags, + ProxyServer = proxyServer, + ProxyBypass = proxyBypass, + AutoConfigUrl = autoConfigUrl, + }; + } + + private static string PtrToString(IntPtr value) + { + return value == IntPtr.Zero ? string.Empty : Marshal.PtrToStringUni(value) ?? string.Empty; + } + + private static void FreeGlobalString(IntPtr value) + { + if (value != IntPtr.Zero) + { + GlobalFree(value); + } + } + + private sealed class SetOption : IDisposable + { + private IntPtr allocatedString; + + private SetOption(InternetPerConnectionOption native, IntPtr allocatedString = default) + { + Native = native; + this.allocatedString = allocatedString; + } + + public InternetPerConnectionOption Native { get; } + + public static SetOption Flags(InternetPerConnectionFlags flags) + { + return new(new() + { + Option = InternetPerConnFlags, + Value = new() { IntValue = (int)flags }, + }); + } + + public static SetOption String(int option, string value) + { + IntPtr pointer = Marshal.StringToHGlobalUni(value ?? string.Empty); + return new(new() + { + Option = option, + Value = new() { StringValue = pointer }, + }, pointer); + } + + public void Dispose() + { + if (allocatedString != IntPtr.Zero) + { + Marshal.FreeHGlobal(allocatedString); + allocatedString = IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct WinHttpCurrentUserIeProxyConfig + { + [MarshalAs(UnmanagedType.Bool)] + public bool AutoDetect; + public IntPtr AutoConfigUrl; + public IntPtr Proxy; + public IntPtr ProxyBypass; + } + + // INTERNET_PER_CONN_OPTION.Value is a native union of DWORD, pointer and FILETIME. + // FILETIME keeps the native union explicitly 8 bytes wide; omitting it can + // corrupt INTERNET_PER_CONN_OPTION arrays when marshalled by WinINet. + [StructLayout(LayoutKind.Explicit, Size = 8)] + private struct InternetPerConnectionOptionValue + { + [FieldOffset(0)] + public int IntValue; + + [FieldOffset(0)] + public IntPtr StringValue; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME FileTimeValue; + } + + [StructLayout(LayoutKind.Sequential)] + private struct InternetPerConnectionOption + { + public int Option; + public InternetPerConnectionOptionValue Value; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct InternetPerConnectionOptionList + { + public int Size; + public IntPtr Connection; + public int OptionCount; + public int OptionError; + public IntPtr Options; + } + + [DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool InternetSetOption(IntPtr internet, int option, IntPtr buffer, int bufferLength); + + [DllImport("winhttp.dll", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool WinHttpGetIEProxyConfigForCurrentUser(ref WinHttpCurrentUserIeProxyConfig proxyConfig); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalFree(IntPtr memory); + } +} diff --git a/Shadowsocks.Windows/Util/WindowsSystemUtilities.cs b/Shadowsocks.Windows/Util/WindowsSystemUtilities.cs new file mode 100644 index 000000000..d57ef1337 --- /dev/null +++ b/Shadowsocks.Windows/Util/WindowsSystemUtilities.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.Win32; +using NLog; + +namespace Shadowsocks.Util +{ + public enum WindowsThemeMode + { + Dark, + Light, + } + + /// + /// Windows-only registry and shell preferences. Kept outside Shadowsocks.Core so + /// the core assembly has no dependency on Microsoft.Win32. + /// + public static class WindowsSystemUtilities + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static WindowsThemeMode GetSystemThemeSetting() + { + try + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", false); + object value = key?.GetValue("SystemUsesLightTheme"); + return value is int intValue && intValue != 0 + ? WindowsThemeMode.Light + : WindowsThemeMode.Dark; + } + catch (Exception exception) + { + Logger.Debug(exception, "Cannot read the Windows system theme; using dark mode."); + return WindowsThemeMode.Dark; + } + } + + public static RegistryKey OpenRegistryKey( + string name, + bool writable, + RegistryHive hive = RegistryHive.CurrentUser) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Registry key name must not be empty.", nameof(name)); + + try + { + return RegistryKey.OpenBaseKey( + hive, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32) + .OpenSubKey(name, writable); + } + catch (Exception exception) + { + Logger.LogUsefulException(exception); + return null; + } + } + } +} diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt new file mode 100644 index 000000000..d2dab387a --- /dev/null +++ b/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,143 @@ +# Third-party notices + +This document records third-party software that is compiled into, linked by, derived into, or downloaded by Shadowsocks Reborn 5.2.31. + +The Shadowsocks Reborn application itself is licensed under **GPL-3.0-or-later**. Third-party components remain under their own licenses. A dependency listed here is not relicensed under the Shadowsocks Reborn license. + +> UI localization note: the application displays a localized explanatory introduction for this document. License names, copyright notices, disclaimers and authoritative legal terms below remain in their original language so an unofficial translation cannot alter their meaning. + +## Runtime components included in the application + +| Component | Version | License / terms | Purpose | +| --- | ---: | --- | --- | +| Bouncy Castle Cryptography for .NET | 2.7.0 | MIT-style Bouncy Castle License; bundled modified Bzip2 code is Apache-2.0 | Managed Minisign/Ed25519 verification for DNSCrypt release assets | +| Google.Protobuf | 3.35.1 | BSD-3-Clause | GeoSite/protobuf data parsing | +| Newtonsoft.Json | 13.0.4 | MIT | JSON parsing and configuration/update data | +| NLog | 6.2.0 | BSD-3-Clause | Application logging | +| WinUIEx | 2.9.3 | MIT | WinUI window/tray integration | +| ZXing.Net | 0.16.11 | Apache-2.0 | QR-code support | +| System.Management | 10.0.11 | MIT (.NET Runtime) | Windows management APIs | +| System.Drawing.Common | 10.0.11 | MIT (.NET Runtime) | Windows image/graphics compatibility used by UI integration | +| .NET 10 runtime libraries | 10.0.11 security baseline (release SDK 10.0.303+) | MIT plus the .NET Runtime repository's third-party notices | Self-contained .NET runtime | +| Windows App SDK | 2.4.0 | Microsoft package license terms; source repository is MIT-licensed where stated by Microsoft | Self-contained Windows App SDK runtime | +| Microsoft.WindowsAppSDK.WinUI | 2.3.6 | Microsoft package license terms supplied with the NuGet package | WinUI 3 UI runtime | + +### Bouncy Castle Cryptography for .NET + +Project: https://www.bouncycastle.org/csharp/ +Source: https://github.com/bcgit/bc-csharp +Package: `BouncyCastle.Cryptography` 2.7.0 + +Copyright (c) 2000-2026 The Legion of the Bouncy Castle Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The Bouncy Castle package also contains a modified Bzip2 implementation licensed under Apache License 2.0. See: https://www.apache.org/licenses/LICENSE-2.0 + +### Newtonsoft.Json and MIT-licensed managed dependencies + +Newtonsoft.Json source: https://github.com/JamesNK/Newtonsoft.Json +WinUIEx source: https://github.com/dotMorten/WinUIEx +.NET Runtime source: https://github.com/dotnet/runtime + +These components are distributed under the MIT license (or, for Microsoft package payloads, the package-specific terms identified above). The applicable copyright and license files remain authoritative. + +### Google.Protobuf + +Package: `Google.Protobuf` 3.35.1 +Source: https://github.com/protocolbuffers/protobuf +License: BSD-3-Clause +Copyright: Google Inc. and contributors. + +### NLog + +Package: `NLog` 6.2.0 +Source: https://github.com/NLog/NLog +License: BSD-3-Clause +Copyright (c) 2004-2026 NLog Project. + +### ZXing.Net + +Package: `ZXing.Net` 0.16.11 +Source: https://github.com/micjahn/ZXing.Net +License: Apache-2.0 +Apache License 2.0: https://www.apache.org/licenses/LICENSE-2.0 + +### Windows App SDK / WinUI 3 + +Packages used by the application include `Microsoft.WindowsAppSDK` 2.4.0 and `Microsoft.WindowsAppSDK.WinUI` 2.3.6. The application uses self-contained Windows App SDK deployment. + +Source repository: https://github.com/microsoft/WindowsAppSDK +NuGet package pages: +- https://www.nuget.org/packages/Microsoft.WindowsAppSDK/2.4.0 +- https://www.nuget.org/packages/Microsoft.WindowsAppSDK.WinUI/2.3.6 + +**Important:** binary redistribution is governed by the license terms shipped with the corresponding Microsoft NuGet packages. The MIT license of source portions in the public Windows App SDK repository must not be used to replace package-specific binary license terms. + +## Source-derived code included in the repository + +### ByteCircularBuffer + +File: `Shadowsocks.Core/Encryption/CircularBuffer/ByteCircularBuffer.cs` +License: BSD-3-Clause / New BSD License. + +Copyright (c) 2014-2015 Cyotek Ltd +Copyright (c) 2012 Alex Regueiro +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the copyright notice, conditions and disclaimer in the source file are retained/reproduced as required by the BSD license. The complete original notice is preserved verbatim at the top of `ByteCircularBuffer.cs`. + +### Adblock Plus-derived network routing + +File: `Shadowsocks.Core/Routing/FilterEngine.cs` +Origin: Adblock Plus / Eyeo GmbH network-filter grammar and the historical Shadowsocks PAC adaptation +License: GNU GPL version 3. +Copyright (C) 2006-2014 Eyeo GmbH. + +Shadowsocks Reborn reimplements the network-routing subset needed by Local PAC/GeoSite in managed C#. The historical bundled `abp.js` runtime, the compiled-PAC compatibility backend, and the executable custom `abp.txt` override are no longer distributed or supported. Local GeoSite/EasyList/ABP routing is authoritative in the C# `FilterEngine`; the Local PAC itself is only a minimal Windows proxy funnel. Online PAC is a separate user-selected external PAC feature. + +## Optional runtime components downloaded on demand + +These executables/drivers are **not included in the Shadowsocks Reborn GitHub release ZIP**. The application downloads official upstream artifacts only when the corresponding feature is requested. + +### dnscrypt-proxy + +Version: selected verified upstream release +Project/source: https://github.com/DNSCrypt/dnscrypt-proxy +License: ISC. + +`dnscrypt-proxy.exe` is not bundled in `Shadowsocks-win-x64.zip`. The application downloads the official Windows x64 release and verifies the signed release material before activation. The downloaded component remains an independent work under the DNSCrypt project's ISC license. + +### WinDivert + +Version: 2.2.2 +Project/source: https://github.com/basil00/WinDivert +License: user's choice of GNU LGPL version 3 or GNU GPL version 2. + +`WinDivert.dll` and `WinDivert64.sys` are not bundled in `Shadowsocks-win-x64.zip`. Administrator Mode downloads the pinned official WinDivert 2.2.2 x64 archive and verifies pinned SHA-256 hashes before using the runtime. Complete upstream license terms are available in the WinDivert `LICENSE` file. + +## Build-time tooling (not shipped in the release executable) + +| Component | Version | License | Role | +| --- | ---: | --- | --- | +| Fody | 6.9.3 | MIT | Build-time IL weaving infrastructure | +| Caseless.Fody | 1.9.1 | MIT | Build-time weaving plugin | +| Microsoft.Windows.SDK.BuildTools | 10.0.28000.2526 | Microsoft package terms | Windows SDK build tooling | + +Build/test-only packages such as MSTest and Microsoft.NET.Test.Sdk are development dependencies and are not part of the product release artifact. + +## Source and corresponding source + +Shadowsocks Reborn source code, build scripts and release metadata are published at: + +https://github.com/SlimRG/shadowsocks-reborn + +For third-party components, the upstream source URLs above identify the corresponding projects. Exact package versions used for release builds are pinned in the repository `.csproj` files. + +## No warranty + +Third-party names and trademarks belong to their respective owners. Inclusion of a component or reference to an upstream project does not imply endorsement. Each component is provided subject to its own warranty disclaimer and license terms. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 000000000..9878fc7fe --- /dev/null +++ b/TODO.txt @@ -0,0 +1,4 @@ +Shadowsocks Reborn TODO + +SIP003 plugins +- Custom repository sources: design repository trust/validation, release asset-selection rules, provenance and UI warnings, and regression tests before exposing an arbitrary repository field. Background auto-update intentionally remains limited to the trusted built-in catalog; manual ZIP/TAR.GZ import remains supported. diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 000000000..509002151 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "FilePath": "shadowsocks.log", + "FallbackFilePath": "shadowsocks.log", + "MinimumLevel": "Info", + "VerboseMinimumLevel": "Debug", + "ArchiveAboveSizeBytes": 10485760, + "MaxArchiveFiles": 7, + "MaxArchiveDays": 7, + "Layout": "${longdate}|${level:uppercase=true}|${logger}|${message}${onexception:inner=${newline}${exception:format=tostring}}" + } +} diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 8fa2a3e83..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,156 +0,0 @@ - -# Notes: -# - Minimal appveyor.yml file is an empty file. All sections are optional. -# - Indent each level of configuration with 2 spaces. Do not use tabs! -# - All section names are case-sensitive. -# - Section names should be unique on each level. - -#---------------------------------# -# general configuration # -#---------------------------------# - -# version format -# Build version format is taken from UI if it is not set -version: 4.4.1.{build} - -# # branches to build -# branches: -# # whitelist -# only: -# - master -# - production - -# # blacklist -# except: -# - gh-pages - - -#---------------------------------# -# environment configuration # -#---------------------------------# - -# Build worker image (VM template) -image: Visual Studio 2019 - -# scripts that are called at very beginning, before repo cloning -# init: -# - git config --global core.autocrlf false - - -# set clone depth -clone_depth: 5 # clone entire repository history if not defined - - -# environment variables -environment: - # my_var1: value1 - # # this is how to set encrypted variable. Go to "Settings" -> "Encrypt YAML" page in account menu to encrypt data. - # my_secure_var1: - # secure: FW3tJ3fMncxvs58/ifSP7w== - matrix: - - platform: x86 - configuration: Debug - - platform: x86 - configuration: Release - -# this is how to allow failing jobs in the matrix -matrix: - fast_finish: false # set this flag to immediately finish build once one of the jobs fails. - -# build cache to preserve files/folders between builds -cache: - - packages -> **\packages.config # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified - # - '%LocalAppData%\NuGet\Cache' # NuGet < v3 - - '%LocalAppData%\NuGet\v3-cache' # NuGet v3 - - -# Automatically register private account and/or project AppVeyor NuGet feeds. -# nuget: - # account_feed: true - # project_feed: true - # disable_publish_on_pr: true # disable publishing of .nupkg artifacts to account/project feeds for pull request builds - # publish_wap_octopus: true # disable publishing of Octopus Deploy .nupkg artifacts to account/project feeds - -#---------------------------------# -# build configuration # -#---------------------------------# - -# Build settings, not to be confused with "before_build" and "after_build". -# "project" is relative to the original build directory and not influenced by directory changes in "before_build". -build: - # parallel: true # enable MSBuild parallel builds - # publish_nuget: true # package projects with .nuspec files and push to artifacts - # publish_nuget_symbols: true # generate and publish NuGet symbol packages - # include_nuget_references: true # add -IncludeReferencedProjects option while packaging NuGet artifacts - - # MSBuild verbosity level - verbosity: normal # quiet|minimal|normal|detailed - - -# scripts to run before build -before_build: - - cmd: nuget restore - -# to run your custom scripts instead of automatic MSBuild -# build_script: - -# scripts to run after build (working directory and environment changes are persisted from the previous steps) -after_build: -- ps: |+ - function CalculateHash($file) - { - $newLine = "`r`n" - $text = (Split-Path $file -Leaf) + $newLine - $text += 'MD5' + $newLine - $text += (Get-FileHash $file -Algorithm MD5).Hash + $newLine - $text += 'SHA-1' + $newLine - $text += (Get-FileHash $file -Algorithm SHA1).Hash + $newLine - $text += 'SHA-256' + $newLine - $text += (Get-FileHash $file -Algorithm SHA256).Hash + $newLine - $text += 'SHA-512' + $newLine - $text += (Get-FileHash $file -Algorithm SHA512).Hash - return $text - } - - $WorkingFolder = "$env:APPVEYOR_BUILD_FOLDER\shadowsocks-csharp\bin\$env:PLATFORM\$env:CONFIGURATION" - $ReleaseFile = "$WorkingFolder\Shadowsocks.exe" - $ReleaseHashFile = "$ReleaseFile.hash" - $ReleaseLocalizationFiles = "$WorkingFolder\*\" - $ZipFile = "$WorkingFolder\Shadowsocks-$env:APPVEYOR_BUILD_VERSION.zip" - $ZipHashFile = "$ZipFile.hash" - - CalculateHash -file "$ReleaseFile" | Out-File -FilePath "$ReleaseHashFile" - 7z a "$ZipFile" "$ReleaseFile" "$ReleaseHashFile" "$ReleaseLocalizationFiles" - Push-AppveyorArtifact "$ZipFile" - - # Calculate packed zip Hash - CalculateHash -file "$ZipFile" | Out-File -FilePath "$ZipHashFile" - Push-AppveyorArtifact "$ZipHashFile" - - -# scripts to run *after* solution is built and *before* automatic packaging occurs (web apps, NuGet packages, Azure Cloud Services) -# before_package: - -# to disable automatic builds -#build: off - - -#---------------------------------# -# deployment configuration # -#---------------------------------# - -# providers: Local, FTP, WebDeploy, AzureCS, AzureBlob, S3, NuGet, Environment -# provider names are case-sensitive! -# deploy: - -# # scripts to run before deployment -# before_deploy: - -# # scripts to run after deployment -# after_deploy: - -# # to run your custom scripts instead of provider deployments -# deploy_script: - -# # to disable deployment -#deploy: off \ No newline at end of file diff --git a/appveyor.yml.obsolete b/appveyor.yml.obsolete deleted file mode 100644 index d23aa9974..000000000 --- a/appveyor.yml.obsolete +++ /dev/null @@ -1,24 +0,0 @@ -# Created by wongsyrone - -version: 1.0.{build} -image: Visual Studio 2017 -environment: - matrix: - - platform: x86 - configuration: Debug - - platform: x86 - configuration: Release -matrix: - fast_finish: false -nuget: - project_feed: true -before_build: -- cmd: nuget restore -build: - parallel: true - verbosity: normal -artifacts: -- path: shadowsocks-csharp\bin\x86\Release\Shadowsocks.exe - name: Shadowsocks-release.exe -- path: shadowsocks-csharp\bin\x86\Debug\Shadowsocks.exe - name: Shadowsocks-debug.exe diff --git a/appveyor.yml.sample b/appveyor.yml.sample deleted file mode 100644 index e3bd655a0..000000000 --- a/appveyor.yml.sample +++ /dev/null @@ -1,483 +0,0 @@ - -# Notes: -# - Minimal appveyor.yml file is an empty file. All sections are optional. -# - Indent each level of configuration with 2 spaces. Do not use tabs! -# - All section names are case-sensitive. -# - Section names should be unique on each level. - -#---------------------------------# -# general configuration # -#---------------------------------# - -# version format -version: 1.0.{build} - -# you can use {branch} name in version format too -# version: 1.0.{build}-{branch} - -# branches to build -branches: - # whitelist - only: - - master - - production - - # blacklist - except: - - gh-pages - -# Do not build on tags (GitHub and BitBucket) -skip_tags: true - -# Start builds on tags only (GitHub and BitBucket) -skip_non_tags: true - -# Skipping commits with particular message or from specific user -skip_commits: - message: /Created.*\.(png|jpg|jpeg|bmp|gif)/ # Regex for matching commit message - author: John # Commit author's username, name, email or regexp maching one of these. - -# Including commits with particular message or from specific user -only_commits: - message: /build/ # Start a new build if message contains 'build' - author: jack@company.com # Start a new build for commit of user with email jack@company.com - -# Skipping commits affecting specific files (GitHub only). More details here: /docs/appveyor-yml -#skip_commits: -# files: -# - docs/* -# - '**/*.html' - -# Including commits affecting specific files (GitHub only). More details here: /docs/appveyor-yml -#only_commits: -# files: -# - Project-A/ -# - Project-B/ - -# Do not build feature branch with open Pull Requests -skip_branch_with_pr: true - -# Maximum number of concurrent jobs for the project -max_jobs: 1 - -#---------------------------------# -# environment configuration # -#---------------------------------# - -# Build worker image (VM template) -image: Visual Studio 2015 - -# scripts that are called at very beginning, before repo cloning -init: - - git config --global core.autocrlf input - -# clone directory -clone_folder: c:\projects\myproject - -# fetch repository as zip archive -shallow_clone: true # default is "false" - -# set clone depth -clone_depth: 5 # clone entire repository history if not defined - -# setting up etc\hosts file -hosts: - queue-server: 127.0.0.1 - db.server.com: 127.0.0.2 - -# environment variables -environment: - my_var1: value1 - my_var2: value2 - # this is how to set encrypted variable. Go to "Settings" -> "Encrypt YAML" page in account menu to encrypt data. - my_secure_var1: - secure: FW3tJ3fMncxvs58/ifSP7w== - -# environment: -# global: -# connection_string: server=12;password=13; -# service_url: https://127.0.0.1:8090 -# -# matrix: -# - db: mysql -# provider: mysql -# -# - db: mssql -# provider: mssql -# password: -# secure: $#(JFDA)jQ@#$ - -# this is how to allow failing jobs in the matrix -matrix: - fast_finish: true # set this flag to immediately finish build once one of the jobs fails. - allow_failures: - - platform: x86 - configuration: Debug - - platform: x64 - configuration: Release - -# exclude configuration from the matrix. Works similarly to 'allow_failures' but build not even being started for excluded combination. - exclude: - - platform: x86 - configuration: Debug - -# build cache to preserve files/folders between builds -cache: - - packages -> **\packages.config # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified - - projectA\libs - - node_modules # local npm modules - - '%LocalAppData%\NuGet\Cache' # NuGet < v3 - - '%LocalAppData%\NuGet\v3-cache' # NuGet v3 - -# enable service required for build/tests -services: - - mssql2014 # start SQL Server 2014 Express - - mssql2014rs # start SQL Server 2014 Express and Reporting Services - - mssql2012sp1 # start SQL Server 2012 SP1 Express - - mssql2012sp1rs # start SQL Server 2012 SP1 Express and Reporting Services - - mssql2008r2sp2 # start SQL Server 2008 R2 SP2 Express - - mssql2008r2sp2rs # start SQL Server 2008 R2 SP2 Express and Reporting Services - - mysql # start MySQL 5.6 service - - postgresql # start PostgreSQL 9.5 service - - iis # start IIS - - msmq # start Queuing services - - mongodb # start MongoDB - -# scripts that run after cloning repository -install: - # by default, all script lines are interpreted as batch - - echo This is batch - # to run script as a PowerShell command prepend it with ps: - - ps: Write-Host 'This is PowerShell' - # batch commands start from cmd: - - cmd: echo This is batch again - - cmd: set MY_VAR=12345 - -# enable patching of AssemblyInfo.* files -assembly_info: - patch: true - file: AssemblyInfo.* - assembly_version: "2.2.{build}" - assembly_file_version: "{version}" - assembly_informational_version: "{version}" - - -# Automatically register private account and/or project AppVeyor NuGet feeds. -nuget: - account_feed: true - project_feed: true - disable_publish_on_pr: true # disable publishing of .nupkg artifacts to account/project feeds for pull request builds - publish_wap_octopus: true # disable publishing of Octopus Deploy .nupkg artifacts to account/project feeds - -#---------------------------------# -# build configuration # -#---------------------------------# - -# build platform, i.e. x86, x64, Any CPU. This setting is optional. -platform: Any CPU - -# to add several platforms to build matrix: -#platform: -# - x86 -# - Any CPU - -# build Configuration, i.e. Debug, Release, etc. -configuration: Release - -# to add several configurations to build matrix: -#configuration: -# - Debug -# - Release - -# Build settings, not to be confused with "before_build" and "after_build". -# "project" is relative to the original build directory and not influenced by directory changes in "before_build". -build: - parallel: true # enable MSBuild parallel builds - project: MyTestAzureCS.sln # path to Visual Studio solution or project - publish_wap: true # package Web Application Projects (WAP) for Web Deploy - publish_wap_xcopy: true # package Web Application Projects (WAP) for XCopy deployment - publish_wap_beanstalk: true # Package Web Applications for AWS Elastic Beanstalk deployment - publish_wap_octopus: true # Package Web Applications for Octopus deployment - publish_azure_webjob: true # Package Azure WebJobs for Zip Push deployment - publish_azure: true # package Azure Cloud Service projects and push to artifacts - publish_aspnet_core: true # Package ASP.NET Core projects - publish_core_console: true # Package .NET Core console projects - publish_nuget: true # package projects with .nuspec files and push to artifacts - publish_nuget_symbols: true # generate and publish NuGet symbol packages - include_nuget_references: true # add -IncludeReferencedProjects option while packaging NuGet artifacts - - # MSBuild verbosity level - verbosity: quiet|minimal|normal|detailed - - -# scripts to run before build -before_build: - -# to run your custom scripts instead of automatic MSBuild -build_script: - -# scripts to run after build (working directory and environment changes are persisted from the previous steps) -after_build: - -# scripts to run *after* solution is built and *before* automatic packaging occurs (web apps, NuGet packages, Azure Cloud Services) -before_package: - -# to disable automatic builds -#build: off - -#---------------------------------# -# tests configuration # -#---------------------------------# - -# to run tests against only selected assemblies and/or categories -test: - assemblies: - only: - - asm1.dll - - asm2.dll - - categories: - only: - - UI - - E2E - -# to run tests against all except selected assemblies and/or categories -#test: -# assemblies: -# except: -# - asm1.dll -# - asm2.dll -# -# categories: -# except: -# - UI -# - E2E - -# to run tests from different categories as separate jobs in parallel -#test: -# categories: -# - A # A category common for all jobs -# - [UI] # 1st job -# - [DAL, BL] # 2nd job - -# scripts to run before tests (working directory and environment changes are persisted from the previous steps such as "before_build") -before_test: - - echo script1 - - ps: Write-Host "script1" - -# to run your custom scripts instead of automatic tests -test_script: - - echo This is my custom test script - -# scripts to run after tests -after_test: - -# to disable automatic tests -#test: off - - -#---------------------------------# -# artifacts configuration # -#---------------------------------# - -artifacts: - - # pushing a single file - - path: test.zip - - # pushing a single file with environment variable in path and "Deployment name" specified - - path: MyProject\bin\$(configuration) - name: myapp - - # pushing entire folder as a zip archive - - path: logs - - # pushing all *.nupkg files in build directory recursively - - path: '**\*.nupkg' - - -#---------------------------------# -# deployment configuration # -#---------------------------------# - -# providers: Local, FTP, WebDeploy, AzureCS, AzureBlob, S3, NuGet, Environment -# provider names are case-sensitive! -deploy: - - # FTP deployment provider settings - - provider: FTP - protocol: ftp|ftps|sftp - host: ftp.myserver.com - username: admin - password: - secure: eYKZKFkkEvFYWX6NfjZIVw== - folder: - application: - active_mode: false - beta: true # enable alternative FTP library for 'ftp' and 'ftps' modes - debug: true # show complete FTP log - - # Amazon S3 deployment provider settings - - provider: S3 - access_key_id: - secure: ABcd== - secret_access_key: - secure: ABcd== - bucket: my_bucket - folder: - artifact: - set_public: false - - # Azure Blob storage deployment provider settings - - provider: AzureBlob - storage_account_name: - secure: ABcd== - storage_access_key: - secure: ABcd== - container: my_container - folder: - artifact: - - # Web Deploy deployment provider settings - - provider: WebDeploy - server: http://www.deploy.com/myendpoint - website: mywebsite - username: user - password: - secure: eYKZKFkkEvFYWX6NfjZIVw== - ntlm: false - remove_files: false - app_offline: false - do_not_use_checksum: true # do not use check sum for comparing source and destination files. By default checksums are used. - sync_retry_attempts: 2 # sync attempts, max - sync_retry_interval: 2000 # timeout between sync attempts, milliseconds - aspnet_core: true # artifact zip contains ASP.NET Core application - aspnet_core_force_restart: true # poke app's web.config before deploy to force application restart - skip_dirs: \\App_Data - skip_files: web.config - on: - branch: release - platform: x86 - configuration: debug - - # Deploying to Azure Cloud Service - - provider: AzureCS - subscription_id: - secure: fjZIVw== - subscription_certificate: - secure: eYKZKFkkEv...FYWX6NfjZIVw== - storage_account_name: my_storage - storage_access_key: - secure: ABcd== - service: my_service - slot: Production - target_profile: Cloud - artifact: MyPackage.cspkg - - # Deploying to NuGet feed - - provider: NuGet - server: https://my.nuget.server/feed - api_key: - secure: FYWX6NfjZIVw== - skip_symbols: false - symbol_server: https://your.symbol.server/feed - artifact: MyPackage.nupkg - - # Deploy to GitHub Releases - - provider: GitHub - artifact: /.*\.nupkg/ # upload all NuGet packages to release assets - draft: false - prerelease: false - on: - branch: master # release from master branch only - APPVEYOR_REPO_TAG: true # deploy on tag push only - - # Deploying to a named environment - - provider: Environment - name: staging - on: - branch: staging - env_var1: value1 - env_var2: value2 - -# scripts to run before deployment -before_deploy: - -# scripts to run after deployment -after_deploy: - -# to run your custom scripts instead of provider deployments -deploy_script: - -# to disable deployment -#deploy: off - -#---------------------------------# -# global handlers # -#---------------------------------# - -# on successful build -on_success: - - do something - -# on build failure -on_failure: - - do something - -# after build failure or success -on_finish: - - do something - - -#---------------------------------# -# notifications # -#---------------------------------# - -notifications: - - # Email - - provider: Email - to: - - user1@email.com - - user2@email.com - subject: 'Build {{status}}' # optional - message: "{{message}}, {{commitId}}, ..." # optional - on_build_status_changed: true - - # HipChat - - provider: HipChat - auth_token: - secure: RbOnSMSFKYzxzFRrxM1+XA== - room: ProjectA - template: "{message}, {commitId}, ..." - - # Slack - - provider: Slack - incoming_webhook: http://incoming-webhook-url - - # ...or using auth token - - - provider: Slack - auth_token: - secure: kBl9BlxvRMr9liHmnBs14A== - channel: development - template: "{message}, {commitId}, ..." - - # Campfire - - provider: Campfire - account: appveyor - auth_token: - secure: RifLRG8Vfyol+sNhj9u2JA== - room: ProjectA - template: "{message}, {commitId}, ..." - - # Webhook - - provider: Webhook - url: http://www.myhook2.com - headers: - User-Agent: myapp 1.0 - Authorization: - secure: GhD+5xhLz/tkYY6AO3fcfQ== - on_build_success: false - on_build_failure: true - on_build_status_changed: true diff --git a/global.json b/global.json new file mode 100644 index 000000000..f32a0a2f0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.303", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/packaging/Build-Release.ps1 b/packaging/Build-Release.ps1 new file mode 100644 index 000000000..bd25d81d3 --- /dev/null +++ b/packaging/Build-Release.ps1 @@ -0,0 +1,198 @@ +[CmdletBinding()] +param( + [Parameter()] + [ValidatePattern('^[vV]?\d+\.\d+\.\d+$')] + [string]$Version +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$artifactsRoot = Join-Path $repoRoot 'artifacts' +$publishDir = Join-Path $artifactsRoot 'publish' +$releaseDir = Join-Path $artifactsRoot 'release' +$solutionPath = Join-Path $repoRoot 'shadowsocks-reborn.sln' +$mainProjectPath = Join-Path $repoRoot 'Shadowsocks.WinUI\Shadowsocks.WinUI.csproj' +$coreProjectPath = Join-Path $repoRoot 'Shadowsocks.Core\Shadowsocks.Core.csproj' +$testProjectPath = Join-Path $repoRoot 'Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj' +$minimumReleaseSdk = [Version]'10.0.303' + +function Get-CanonicalVersion { + [CmdletBinding()] + param() + + $propsPath = Join-Path $repoRoot 'Directory.Build.props' + [xml]$props = Get-Content -LiteralPath $propsPath -Raw + $value = [string](@($props.Project.PropertyGroup.Version | Where-Object { $_ })[0]) + if ([string]::IsNullOrWhiteSpace($value) -or $value -notmatch '^\d+\.\d+\.\d+$') { + throw 'Directory.Build.props must contain one stable three-part value.' + } + + return $value.Trim() +} + +function Test-RequiredFile { + [CmdletBinding()] + param([Parameter(Mandatory)][string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "Required release file is missing: $Path" + } +} + +function Invoke-DotNet { + [CmdletBinding()] + param([Parameter(Mandatory)][string[]]$Arguments) + + & dotnet @Arguments + if ($LASTEXITCODE -ne 0) { + throw "dotnet $($Arguments -join ' ') failed with exit code $LASTEXITCODE." + } +} + +function Test-VersionMetadata { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$ReleaseVersion, + [Parameter(Mandatory)][Version]$ExpectedFourPartVersion + ) + + $applicationInfoPath = Join-Path $repoRoot 'Shadowsocks.Core\ApplicationInfo.cs' + $applicationInfoSource = Get-Content -LiteralPath $applicationInfoPath -Raw + if ($applicationInfoSource -notmatch 'public const string Version = "(?\d+(?:\.\d+){3})";') { + throw 'Unable to read ApplicationInfo.Version.' + } + if ([Version]$Matches.version -ne $ExpectedFourPartVersion) { + throw "ApplicationInfo.Version '$($Matches.version)' must match '$ExpectedFourPartVersion'." + } + + foreach ($relativePath in @('Shadowsocks.WinUI\app.manifest', 'Shadowsocks.NetworkService\app.manifest')) { + $manifestPath = Join-Path $repoRoot $relativePath + [xml]$manifest = Get-Content -LiteralPath $manifestPath -Raw + $identity = $manifest.SelectSingleNode("/*[local-name()='assembly']/*[local-name()='assemblyIdentity']") + if ($null -eq $identity) { + throw "Manifest '$relativePath' does not contain assemblyIdentity." + } + + $manifestVersion = [Version]$identity.GetAttribute('version') + if ($manifestVersion -ne $ExpectedFourPartVersion) { + throw "Manifest '$relativePath' version '$manifestVersion' must match '$ExpectedFourPartVersion'." + } + } + + $changesText = Get-Content -LiteralPath (Join-Path $repoRoot 'CHANGES') -Raw + $escapedVersion = [regex]::Escape($ExpectedFourPartVersion.ToString(4)) + if ($changesText -notmatch "(?m)^$escapedVersion \d{4}-\d{2}-\d{2}\s*$") { + throw "CHANGES must contain a dated heading for $ExpectedFourPartVersion." + } + + if ($ReleaseVersion -ne $ExpectedFourPartVersion.ToString(3)) { + throw "Release version '$ReleaseVersion' is inconsistent with '$ExpectedFourPartVersion'." + } +} + +$canonicalVersion = Get-CanonicalVersion +$requestedVersion = if ([string]::IsNullOrWhiteSpace($Version)) { + $canonicalVersion +} +else { + $Version.Trim() -replace '^[vV]', '' +} + +if ($requestedVersion -ne $canonicalVersion) { + throw "Requested release '$requestedVersion' does not match Directory.Build.props version '$canonicalVersion'." +} + +$expectedFourPartVersion = [Version]"$canonicalVersion.0" +Test-VersionMetadata -ReleaseVersion $canonicalVersion -ExpectedFourPartVersion $expectedFourPartVersion + +$dotnetSdkText = (& dotnet --version).Trim() +if ($LASTEXITCODE -ne 0 -or $dotnetSdkText -notmatch '^\d+\.\d+\.\d+$') { + throw "Unable to resolve a stable .NET SDK version. Found '$dotnetSdkText'." +} + +$dotnetSdkVersion = [Version]$dotnetSdkText +if ($dotnetSdkVersion.Major -ne 10 -or $dotnetSdkVersion -lt $minimumReleaseSdk) { + throw "Release build requires .NET 10 SDK $minimumReleaseSdk or newer. Found $dotnetSdkVersion." +} + +Push-Location $repoRoot +try { + Remove-Item -LiteralPath $publishDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $releaseDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -Path $publishDir -ItemType Directory -Force | Out-Null + New-Item -Path $releaseDir -ItemType Directory -Force | Out-Null + + & (Join-Path $PSScriptRoot 'Validate-Repository.ps1') + + Invoke-DotNet -Arguments @('restore', $coreProjectPath, '-p:NuGetAudit=true', '-p:NuGetAuditMode=all') + Invoke-DotNet -Arguments @('build', $coreProjectPath, '-c', 'Release', '--no-restore', '-p:TreatWarningsAsErrors=true') + Invoke-DotNet -Arguments @('restore', $solutionPath, '-p:Platform=x64', '-r', 'win-x64', '-p:NuGetAudit=true', '-p:NuGetAuditMode=all') + Invoke-DotNet -Arguments @('build', $solutionPath, '-c', 'Release', '-p:Platform=x64', '-m:1', '--no-restore', '-p:TreatWarningsAsErrors=true') + Invoke-DotNet -Arguments @('test', $testProjectPath, '-c', 'Release', '-p:Platform=x64', '--no-build') + Invoke-DotNet -Arguments @( + 'restore', $mainProjectPath, + '-p:Platform=x64', + '-p:PublishProfile=FolderProfile', + '-r', 'win-x64', + '-p:NuGetAudit=true', + '-p:NuGetAuditMode=all' + ) + Invoke-DotNet -Arguments @( + 'publish', $mainProjectPath, + '-c', 'Release', + '-p:Platform=x64', + '-p:PublishProfile=FolderProfile', + '-r', 'win-x64', + '--self-contained', 'true', + '-o', $publishDir, + '--no-restore', + '-p:TreatWarningsAsErrors=true' + ) + + $publishedExecutable = Join-Path $publishDir 'Shadowsocks.exe' + Test-RequiredFile $publishedExecutable + + $publishEntries = @(Get-ChildItem -LiteralPath $publishDir -Force) + if ($publishEntries.Count -ne 1 -or $publishEntries[0].PSIsContainer -or $publishEntries[0].Name -ne 'Shadowsocks.exe') { + $publishEntryNames = @($publishEntries | ForEach-Object { $_.Name }) -join ', ' + throw "Final publish must contain exactly Shadowsocks.exe. Found: $publishEntryNames" + } + + $versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($publishedExecutable) + $publishedFileVersion = [Version]::new( + [Math]::Max(0, $versionInfo.FileMajorPart), + [Math]::Max(0, $versionInfo.FileMinorPart), + [Math]::Max(0, $versionInfo.FileBuildPart), + [Math]::Max(0, $versionInfo.FilePrivatePart)) + if ($publishedFileVersion -ne $expectedFourPartVersion) { + throw "Published Shadowsocks.exe FileVersion '$publishedFileVersion' does not match '$expectedFourPartVersion'." + } + + $zipName = 'Shadowsocks-win-x64.zip' + $zipPath = Join-Path $releaseDir $zipName + $hashPath = "$zipPath.sha256" + Compress-Archive -LiteralPath $publishedExecutable -DestinationPath $zipPath -CompressionLevel Optimal -Force + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($zipPath) + try { + $entries = @($archive.Entries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Name) }) + if ($entries.Count -ne 1 -or $entries[0].FullName -ne 'Shadowsocks.exe') { + $entryNames = @($entries | ForEach-Object { $_.FullName }) -join ', ' + throw "Release ZIP must contain exactly Shadowsocks.exe. Found: $entryNames" + } + } + finally { + $archive.Dispose() + } + + $hash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant() + Set-Content -LiteralPath $hashPath -Value "$hash $zipName" -Encoding ascii -NoNewline + + Write-Information "Release package created:`n $zipPath`n $hashPath" -InformationAction Continue +} +finally { + Pop-Location +} diff --git a/packaging/Validate-DnsCrypt.ps1 b/packaging/Validate-DnsCrypt.ps1 new file mode 100644 index 000000000..34d814b52 --- /dev/null +++ b/packaging/Validate-DnsCrypt.ps1 @@ -0,0 +1,103 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Test-Condition { + [CmdletBinding()] + param( + [Parameter(Mandatory)][bool]$Condition, + [Parameter(Mandatory)][string]$Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Get-ProjectPackageVersion { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$ProjectPath, + [Parameter(Mandatory)][string]$PackageName + ) + + [xml]$project = Get-Content -LiteralPath $ProjectPath -Raw + $node = $project.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='$PackageName']") + if ($null -eq $node) { + return $null + } + + return [string]$node.Version +} + +$componentPath = Join-Path $repoRoot 'Shadowsocks.Windows\Controller\Service\DnsCryptComponentManager.cs' +$controllerPath = Join-Path $repoRoot 'Shadowsocks.Windows\Controller\ShadowsocksController.DnsCrypt.cs' +$resolverCatalogBootstrapperPath = Join-Path $repoRoot 'Shadowsocks.Windows\Controller\Service\DnsCryptResolverCatalogBootstrapper.cs' +$winDivertPath = Join-Path $repoRoot 'Shadowsocks.Windows\Controller\Traffic\WinDivertInstaller.cs' +$windowsProjectPath = Join-Path $repoRoot 'Shadowsocks.Windows\Shadowsocks.Windows.csproj' +$testsProjectPath = Join-Path $repoRoot 'Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj' +$noticesPath = Join-Path $repoRoot 'THIRD-PARTY-NOTICES.txt' + +foreach ($path in @($componentPath, $controllerPath, $resolverCatalogBootstrapperPath, $winDivertPath, $windowsProjectPath, $testsProjectPath, $noticesPath)) { + Test-Condition (Test-Path -LiteralPath $path -PathType Leaf) "Required DNS/security file is missing: $path" +} + +$notices = Get-Content -LiteralPath $noticesPath -Raw + +# Concrete security pins are asserted by typed regression tests. This validator intentionally +# checks repository structure, dependencies, notices, and absence of bundled native payloads +# instead of parsing C# implementation text. + +$windowsBouncyCastle = Get-ProjectPackageVersion $windowsProjectPath 'BouncyCastle.Cryptography' +$testsBouncyCastle = Get-ProjectPackageVersion $testsProjectPath 'BouncyCastle.Cryptography' +Test-Condition (-not [string]::IsNullOrWhiteSpace($windowsBouncyCastle)) 'Shadowsocks.Windows must reference BouncyCastle.Cryptography for managed signature verification.' +Test-Condition ($windowsBouncyCastle -eq $testsBouncyCastle) 'BouncyCastle.Cryptography version must match between product and tests.' + +foreach ($notice in @('Bouncy Castle Cryptography for .NET', 'dnscrypt-proxy', 'License: ISC', 'WinDivert', 'Windows App SDK')) { + Test-Condition ($notices.IndexOf($notice, [StringComparison]::OrdinalIgnoreCase) -ge 0) "THIRD-PARTY-NOTICES.txt is missing '$notice'." +} + +$requiredTests = @( + 'DnsCryptBootstrapPolicyTests.cs', + 'DnsCryptCleanModeIntegrationTests.cs', + 'DnsCryptComponentManagerTests.cs', + 'DnsCryptCoordinatorTests.cs', + 'DnsCryptCountrySelectorTests.cs', + 'DnsCryptMaintenancePolicyTests.cs', + 'DnsCryptRuntimeManagerTests.cs', + 'DnsCryptResolverCatalogBootstrapperTests.cs', + 'DnsCryptTomlGeneratorTests.cs', + 'DnsProcessResolverTests.cs', + 'DnsCaptureStage4Tests.cs', + 'WinDivertInstallerTests.cs', + 'IpFragmentTrackerTests.cs', + 'ProcessAttributionCacheTests.cs' +) +foreach ($testFile in $requiredTests) { + $path = Join-Path $repoRoot "Shadowsocks.UnitTests\$testFile" + Test-Condition (Test-Path -LiteralPath $path -PathType Leaf) "Required DNS/WinDivert regression test is missing: $testFile" +} + +$forbiddenNativeNames = @( + 'dnscrypt-proxy.exe', + 'minisign.exe', + 'libsodium.dll', + 'libsodium-23.dll', + 'WinDivert.dll', + 'WinDivert64.sys' +) +$unexpectedNativeFiles = @( + Get-ChildItem -LiteralPath $repoRoot -Recurse -File | + Where-Object { + $_.FullName -notmatch '[\\/](?:\.git|\.github|bin|obj|artifacts)[\\/]' -and + $forbiddenNativeNames -contains $_.Name + } +) +$unexpectedNativeFileNames = @($unexpectedNativeFiles | ForEach-Object { $_.FullName }) -join ', ' +Test-Condition ($unexpectedNativeFiles.Count -eq 0) "Downloaded runtime binaries must not be committed to the source tree: $unexpectedNativeFileNames" + +Write-Information 'DNSCrypt/WinDivert release invariants validated.' -InformationAction Continue diff --git a/packaging/Validate-Repository.ps1 b/packaging/Validate-Repository.ps1 new file mode 100644 index 000000000..beb9a4cf0 --- /dev/null +++ b/packaging/Validate-Repository.ps1 @@ -0,0 +1,412 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$minimumSdk = [Version]'10.0.303' +$expectedLocales = @('en', 'ru-RU', 'zh-CN', 'zh-TW', 'ja', 'ko', 'fr') + +function Test-Condition { + [CmdletBinding()] + param( + [Parameter(Mandatory)][bool]$Condition, + [Parameter(Mandatory)][string]$Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Test-RequiredFile { + [CmdletBinding()] + param([Parameter(Mandatory)][string]$RelativePath) + + $path = Join-Path $repoRoot $RelativePath + Test-Condition (Test-Path -LiteralPath $path -PathType Leaf) "Required file is missing: $RelativePath" +} + +function Get-XmlDocument { + [CmdletBinding()] + param([Parameter(Mandatory)][string]$RelativePath) + + $path = Join-Path $repoRoot $RelativePath + Test-RequiredFile $RelativePath + return [xml](Get-Content -LiteralPath $path -Raw) +} + +function Get-ProjectProperty { + [CmdletBinding()] + param( + [Parameter(Mandatory)][xml]$Project, + [Parameter(Mandatory)][string]$Name + ) + + $node = $Project.SelectSingleNode("/Project/PropertyGroup/$Name") + if ($null -eq $node) { + return $null + } + + return [string]$node.InnerText +} + +function Get-PackageVersion { + [CmdletBinding()] + param( + [Parameter(Mandatory)][xml]$Project, + [Parameter(Mandatory)][string]$PackageName + ) + + $node = $Project.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='$PackageName']") + if ($null -eq $node) { + return $null + } + + return [string]$node.Version +} + +function Get-RepositoryFiles { + [CmdletBinding()] + param() + + return @( + Get-ChildItem -LiteralPath $repoRoot -Recurse -File | + Where-Object { $_.FullName -notmatch '[\\/](?:\.git|\.github|bin|obj|artifacts)[\\/]' } + ) +} + +function Test-PowerShellSyntax { + [CmdletBinding()] + param([Parameter(Mandatory)][System.IO.FileInfo[]]$Files) + + foreach ($file in $Files) { + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors) + if (@($errors).Count -ne 0) { + $messages = $errors | ForEach-Object { "$($_.Extent.StartLineNumber):$($_.Extent.StartColumnNumber) $($_.Message)" } + throw "PowerShell syntax error in '$($file.FullName)': $($messages -join '; ')" + } + } +} + +function Test-TextEncodingAndLineEndings { + [CmdletBinding()] + param([Parameter(Mandatory)][System.IO.FileInfo[]]$Files) + + $extensions = @('.cs', '.csproj', '.props', '.targets', '.pubxml', '.xml', '.xaml', '.resx', '.json', '.md', '.txt', '.csv', '.ps1', '.sln', '.manifest', '.config', '.yml', '.yaml') + foreach ($file in $Files) { + if ($extensions -notcontains $file.Extension.ToLowerInvariant() -and $file.Name -notin @('CHANGES', 'NuGet.Config')) { + continue + } + + $bytes = [System.IO.File]::ReadAllBytes($file.FullName) + Test-Condition ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) "Text file must be UTF-8 with BOM: $($file.FullName)" + $text = [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + Test-Condition ($text -notmatch '(?', [StringComparison]::Ordinal) -lt 0) 'CI must not require a project-local ; projects inherit the canonical version.' + Test-Condition ($ciText.IndexOf('$projectXml.Project.PropertyGroup.Version', [StringComparison]::Ordinal) -lt 0) 'CI must not read Version directly from Shadowsocks.WinUI.csproj.' + Test-Condition ($releaseText.IndexOf('.\packaging\Build-Release.ps1', [StringComparison]::Ordinal) -ge 0) 'Release workflow must package through packaging\Build-Release.ps1.' +} + +function Test-WinUiIsEnabledAssignments { + [CmdletBinding()] + param() + + $winUiRoot = Join-Path $repoRoot 'Shadowsocks.WinUI' + $nonControlTypes = @('Border', 'Grid', 'StackPanel', 'Panel', 'TextBlock', 'Image', 'FontIcon', 'Canvas') + $sourceFiles = @(Get-ChildItem -LiteralPath $winUiRoot -Recurse -File -Filter '*.cs') + + foreach ($file in $sourceFiles) { + $text = Get-Content -LiteralPath $file.FullName -Raw + foreach ($typeName in $nonControlTypes) { + $declarations = [regex]::Matches($text, "\b$([regex]::Escape($typeName))\s+(?_?[A-Za-z][A-Za-z0-9_]*)\b") + foreach ($declaration in $declarations) { + $variableName = $declaration.Groups['name'].Value + $assignmentPattern = "\b$([regex]::Escape($variableName))\.IsEnabled\s*=" + Test-Condition ($text -notmatch $assignmentPattern) "WinUI IsEnabled cannot target non-Control $typeName '$variableName' in $($file.FullName). Disable the interactive child controls instead." + } + } + } +} + +$requiredFiles = @( + '.github\workflows\ci.yml', + '.github\workflows\release.yml', + 'Directory.Build.props', + 'global.json', + 'NuGet.Config', + 'appsettings.json', + 'LICENSE.txt', + 'THIRD-PARTY-NOTICES.txt', + 'README.md', + 'README.ru.md', + 'README.zh-CN.md', + 'CHANGES', + 'shadowsocks-reborn.sln', + 'Shadowsocks.Core\Shadowsocks.Core.csproj', + 'Shadowsocks.Windows\Shadowsocks.Windows.csproj', + 'Shadowsocks.Windows.WinUI\Shadowsocks.Windows.WinUI.csproj', + 'Shadowsocks.NetworkService\Shadowsocks.NetworkService.csproj', + 'Shadowsocks.WinUI\Shadowsocks.WinUI.csproj', + 'Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj', + 'Shadowsocks.WinUI\Properties\PublishProfiles\FolderProfile.pubxml', + 'Shadowsocks.Core\Routing\FilterEngine.cs', + 'Shadowsocks.UnitTests\FilterEngineTests.cs', + 'Shadowsocks.UnitTests\ManagedRoutingArchitectureTests.cs', + 'Shadowsocks.UnitTests\ManagedRoutingIntegrationTests.cs', + 'Shadowsocks.Windows\Controller\Service\PluginManager.cs', + 'Shadowsocks.Windows\Controller\ShadowsocksController.Plugins.Maintenance.cs', + 'Shadowsocks.WinUI\Pages\PluginsPage.cs', + 'Shadowsocks.UnitTests\PluginManagerTests.cs', + 'Shadowsocks.UnitTests\LocalizationServiceTests.cs', + 'TODO.txt' +) +foreach ($file in $requiredFiles) { + Test-RequiredFile $file +} + +$allFiles = Get-RepositoryFiles +Test-PowerShellSyntax @($allFiles | Where-Object Extension -EQ '.ps1') +Test-TextEncodingAndLineEndings $allFiles +Test-PowerShellStrictModeCollectionFormatting +Test-PowerShellReturnStatementUsage +Test-GitHubWorkflowContracts +Test-WinUiIsEnabledAssignments + +$unexpectedBuildTrees = @(Get-ChildItem -LiteralPath $repoRoot -Recurse -Directory | Where-Object { $_.Name -in @('bin', 'obj', 'artifacts') -and $_.FullName -notmatch '[\\/](?:\.git|\.github)[\\/]' }) +$unexpectedBuildTreeNames = @($unexpectedBuildTrees | ForEach-Object { $_.FullName }) -join ', ' +Test-Condition ($unexpectedBuildTrees.Count -eq 0) "Source archive must not contain build output directories: $unexpectedBuildTreeNames" + +$javascriptFiles = @($allFiles | Where-Object { $_.Extension.ToLowerInvariant() -in @('.js', '.mjs', '.cjs') }) +$javascriptFileNames = @($javascriptFiles | ForEach-Object { $_.FullName }) -join ', ' +Test-Condition ($javascriptFiles.Count -eq 0) "Bundled JavaScript is not allowed in the source tree: $javascriptFileNames" + +$forbiddenProjectDirectories = @('Shadowsocks.Engine', 'Shadowsocks.UI') +foreach ($directory in $forbiddenProjectDirectories) { + Test-Condition (-not (Test-Path -LiteralPath (Join-Path $repoRoot $directory))) "Retired project directory remains: $directory" +} + +$projectFiles = @($allFiles | Where-Object Extension -EQ '.csproj') +Test-Condition ($projectFiles.Count -eq 6) "Expected exactly six project files; found $($projectFiles.Count)." + +$props = Get-XmlDocument 'Directory.Build.props' +$canonicalVersion = Get-ProjectProperty $props 'Version' +Test-Condition ($canonicalVersion -match '^\d+\.\d+\.\d+$') 'Directory.Build.props must define one stable three-part Version.' +Test-Condition ((Get-ProjectProperty $props 'AnalysisLevel') -eq '10.0-recommended') 'AnalysisLevel must be 10.0-recommended.' +Test-Condition ((Get-ProjectProperty $props 'EnforceCodeStyleInBuild') -eq 'true') 'EnforceCodeStyleInBuild must be true.' +$nugetAuditNode = $props.SelectSingleNode("/Project/PropertyGroup/NuGetAudit") +Test-Condition ($null -ne $nugetAuditNode -and [string]$nugetAuditNode.InnerText -eq 'false') 'NuGetAudit must default to false for deterministic local builds.' +$warningsAsErrors = [string]($props.SelectSingleNode('/Project/PropertyGroup/WarningsAsErrors').InnerText) +foreach ($code in @('NU1900', 'NU1901', 'NU1902', 'NU1903', 'NU1904')) { + Test-Condition ($warningsAsErrors -match "(?:^|;)$code(?:;|$)") "NuGet audit warning $code must be release-blocking when audit is enabled." +} + +$projectVersions = @($projectFiles | ForEach-Object { ([xml](Get-Content -LiteralPath $_.FullName -Raw)).SelectNodes('/Project/PropertyGroup/Version') } | Where-Object { $null -ne $_ }) +Test-Condition ($projectVersions.Count -eq 0) 'Project files must inherit the canonical Version from Directory.Build.props instead of duplicating it.' + +$global = Get-Content -LiteralPath (Join-Path $repoRoot 'global.json') -Raw | ConvertFrom-Json +$sdkVersion = [Version]$global.sdk.version +Test-Condition ($sdkVersion.Major -eq 10 -and $sdkVersion -ge $minimumSdk) "global.json must pin .NET 10 SDK $minimumSdk or newer." +Test-Condition ($global.sdk.rollForward -eq 'latestFeature') 'global.json rollForward must be latestFeature.' +Test-Condition ($global.sdk.allowPrerelease -eq $false) 'global.json must disable prerelease SDKs.' + +[xml]$nugetConfig = Get-Content -LiteralPath (Join-Path $repoRoot 'NuGet.Config') -Raw +$packageSources = @($nugetConfig.configuration.packageSources.add) +Test-Condition ($packageSources.Count -eq 1 -and $packageSources[0].key -eq 'nuget.org' -and $packageSources[0].value -eq 'https://api.nuget.org/v3/index.json' -and $packageSources[0].protocolVersion -eq '3') 'NuGet.Config must use only the official nuget.org v3 package source.' +$restoreSettings = @($nugetConfig.configuration.packageRestore.add) +$restoreEnabled = @($restoreSettings | Where-Object { $_.key -eq 'enabled' -and $_.value -eq 'True' }).Count -eq 1 +$automaticRestore = @($restoreSettings | Where-Object { $_.key -eq 'automatic' -and $_.value -eq 'True' }).Count -eq 1 +Test-Condition ($restoreSettings.Count -eq 2 -and $restoreEnabled -and $automaticRestore) 'NuGet.Config must keep package restore enabled and automatic.' + +$core = Get-XmlDocument 'Shadowsocks.Core\Shadowsocks.Core.csproj' +$windows = Get-XmlDocument 'Shadowsocks.Windows\Shadowsocks.Windows.csproj' +$windowsWinUI = Get-XmlDocument 'Shadowsocks.Windows.WinUI\Shadowsocks.Windows.WinUI.csproj' +$networkService = Get-XmlDocument 'Shadowsocks.NetworkService\Shadowsocks.NetworkService.csproj' +$winUI = Get-XmlDocument 'Shadowsocks.WinUI\Shadowsocks.WinUI.csproj' +$tests = Get-XmlDocument 'Shadowsocks.UnitTests\Shadowsocks.UnitTests.csproj' +$publishProfile = Get-XmlDocument 'Shadowsocks.WinUI\Properties\PublishProfiles\FolderProfile.pubxml' + +Test-Condition ((Get-ProjectProperty $core 'TargetFramework') -eq 'net10.0') 'Shadowsocks.Core must target net10.0.' +Test-Condition ((Get-ProjectProperty $windows 'TargetFramework') -eq 'net10.0-windows10.0.19041.0') 'Shadowsocks.Windows target framework changed unexpectedly.' +Test-Condition ((Get-ProjectProperty $windowsWinUI 'TargetFramework') -eq 'net10.0-windows10.0.26100.0') 'Shadowsocks.Windows.WinUI target framework changed unexpectedly.' +Test-Condition ((Get-ProjectProperty $networkService 'TargetFramework') -eq 'net10.0-windows10.0.19041.0') 'NetworkService target framework changed unexpectedly.' +Test-Condition ((Get-ProjectProperty $winUI 'TargetFramework') -eq 'net10.0-windows10.0.26100.0') 'Shadowsocks.WinUI target framework changed unexpectedly.' +Test-Condition ((Get-ProjectProperty $tests 'TargetFramework') -eq 'net10.0-windows10.0.19041.0') 'Unit test target framework changed unexpectedly.' +foreach ($project in @($windows, $windowsWinUI, $networkService, $winUI, $tests)) { + Test-Condition ((Get-ProjectProperty $project 'PlatformTarget') -eq 'x64') 'Windows/runtime projects and tests must target x64.' +} + +$coreReferences = @($core.SelectNodes('/Project/ItemGroup/ProjectReference')) +Test-Condition ($coreReferences.Count -eq 0) 'Shadowsocks.Core must not depend on another project.' +$windowsReferences = @($windows.SelectNodes('/Project/ItemGroup/ProjectReference') | ForEach-Object { [string]$_.Include }) +Test-Condition ($windowsReferences.Count -eq 1 -and $windowsReferences[0] -eq '..\Shadowsocks.Core\Shadowsocks.Core.csproj') 'Shadowsocks.Windows must reference only Shadowsocks.Core.' +$winUIReferences = @($winUI.SelectNodes('/Project/ItemGroup/ProjectReference') | ForEach-Object { [string]$_.Include }) +foreach ($expectedReference in @('..\Shadowsocks.Core\Shadowsocks.Core.csproj', '..\Shadowsocks.Windows\Shadowsocks.Windows.csproj', '..\Shadowsocks.Windows.WinUI\Shadowsocks.Windows.WinUI.csproj')) { + Test-Condition ($winUIReferences -contains $expectedReference) "Shadowsocks.WinUI is missing project reference '$expectedReference'." +} +Test-Condition ($winUIReferences.Count -eq 3) 'Shadowsocks.WinUI must have exactly the expected three project references.' + +$embeddedResources = @($core.SelectNodes('/Project/ItemGroup/EmbeddedResource') | ForEach-Object { [string]$_.Include }) +foreach ($resource in @('Data\i18n.csv', '..\appsettings.json', '..\LICENSE.txt', '..\THIRD-PARTY-NOTICES.txt', 'Data\user-rule.txt')) { + Test-Condition ($embeddedResources -contains $resource) "Core embedded resource is missing: $resource" +} +Test-Condition (-not ($embeddedResources | Where-Object { $_ -match 'abp\.js$' })) 'Historical abp.js must not be embedded.' + +$linkedFilter = $networkService.SelectSingleNode("/Project/ItemGroup/Compile[@Include='..\Shadowsocks.Core\Routing\FilterEngine.cs']") +Test-Condition ($null -ne $linkedFilter -and [string]$linkedFilter.Link -eq 'Routing\Shared\FilterEngine.cs') 'NetworkService must compile the shared managed FilterEngine source.' + +$winUIComponentVersion = Get-PackageVersion $winUI 'Microsoft.WindowsAppSDK.WinUI' +$windowsWinUIComponentVersion = Get-PackageVersion $windowsWinUI 'Microsoft.WindowsAppSDK.WinUI' +Test-Condition (-not [string]::IsNullOrWhiteSpace($winUIComponentVersion) -and $winUIComponentVersion -eq $windowsWinUIComponentVersion) 'Microsoft.WindowsAppSDK.WinUI package version must match across WinUI projects.' + +$publishInvariants = @{ + Configuration = 'Release' + Platform = 'x64' + RuntimeIdentifier = 'win-x64' + WindowsPackageType = 'None' + WindowsAppSDKSelfContained = 'true' + SelfContained = 'true' + PublishSingleFile = 'true' + IncludeAllContentForSelfExtract = 'true' + IncludeNativeLibrariesForSelfExtract = 'true' + EnableCompressionInSingleFile = 'true' + PublishTrimmed = 'false' + PublishReadyToRun = 'false' + DebugSymbols = 'false' +} +foreach ($property in $publishInvariants.GetEnumerator()) { + Test-Condition ((Get-ProjectProperty $publishProfile $property.Key) -eq $property.Value) "Publish profile property '$($property.Key)' must be '$($property.Value)'." +} + +foreach ($targetName in @('PrepareEmbeddedNetworkService', 'ValidateFinalSingleFileLayout')) { + Test-Condition ($null -ne $winUI.SelectSingleNode("/Project/Target[@Name='$targetName']")) "Shadowsocks.WinUI is missing MSBuild target '$targetName'." +} +Test-Condition ($null -ne $networkService.SelectSingleNode("/Project/Target[@Name='ValidateNetworkServicePublishMode']")) 'NetworkService publish validation target is missing.' + +$appSettings = Get-Content -LiteralPath (Join-Path $repoRoot 'appsettings.json') -Raw | ConvertFrom-Json +Test-Condition ($null -ne $appSettings.Logging) 'appsettings.json must contain Logging settings.' +foreach ($name in @('FilePath', 'FallbackFilePath', 'MinimumLevel', 'VerboseMinimumLevel', 'ArchiveAboveSizeBytes', 'MaxArchiveFiles', 'MaxArchiveDays', 'Layout')) { + Test-Condition ($null -ne $appSettings.Logging.$name) "Logging setting '$name' is missing." +} + +$i18nPath = Join-Path $repoRoot 'Shadowsocks.Core\Data\i18n.csv' +Test-RequiredFile 'Shadowsocks.Core\Data\i18n.csv' +$i18nRows = @(Import-Csv -LiteralPath $i18nPath) +Test-Condition ($i18nRows.Count -gt 0) 'i18n.csv must contain translations.' +$actualLocales = @($i18nRows[0].PSObject.Properties.Name) +Test-Condition (($actualLocales -join ',') -eq ($expectedLocales -join ',')) "i18n.csv locale columns must be: $($expectedLocales -join ', ')." +# CsvLocalizationService uses StringComparer.Ordinal, so validation must reject only +# exact duplicate source keys. PowerShell Group-Object is case-insensitive by default and +# would incorrectly collapse intentional keys such as 'Active' and 'ACTIVE'. +$seenEnglishKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +$duplicateKeys = [System.Collections.Generic.List[string]]::new() +foreach ($row in $i18nRows) { + $englishKey = ([string]$row.en).Trim() + if ([string]::IsNullOrWhiteSpace($englishKey) -or $englishKey.StartsWith('#')) { + continue + } + + if (-not $seenEnglishKeys.Add($englishKey) -and -not $duplicateKeys.Contains($englishKey)) { + $duplicateKeys.Add($englishKey) + } +} +$duplicateKeyNames = @($duplicateKeys | Sort-Object) -join ', ' +Test-Condition ($duplicateKeys.Count -eq 0) "i18n.csv contains exact duplicate English keys: $duplicateKeyNames" +foreach ($row in $i18nRows) { + if ([string]$row.en -match '^#') { + continue + } + + foreach ($locale in $expectedLocales) { + Test-Condition (-not [string]::IsNullOrWhiteSpace([string]$row.$locale)) "i18n.csv has an empty '$locale' translation for key '$($row.en)'." + } + + Test-Condition ([string]$row.'ru-RU' -notmatch '[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af]') "i18n.csv has CJK text in ru-RU for key '$($row.en)'." +} + +$xsdFiles = @($allFiles | Where-Object Extension -EQ '.xsd') +$xsdFileNames = @($xsdFiles | ForEach-Object { $_.FullName }) -join ', ' +Test-Condition ($xsdFiles.Count -eq 0) "Generated XSD files must not be stored in the source tree: $xsdFileNames" +foreach ($weaverPath in @('Shadowsocks.Core\FodyWeavers.xml', 'Shadowsocks.Windows\FodyWeavers.xml')) { + Test-RequiredFile $weaverPath + $weaverText = Get-Content -LiteralPath (Join-Path $repoRoot $weaverPath) -Raw + Test-Condition ($weaverText -notmatch 'schemaLocation') "$weaverPath must not reference generated XSD schemas." +} + +$rootMarkdown = @(Get-ChildItem -LiteralPath $repoRoot -File -Filter '*.md' | ForEach-Object Name | Sort-Object) +$expectedRootMarkdown = @('README.md', 'README.ru.md', 'README.zh-CN.md') +Test-Condition (($rootMarkdown -join ',') -eq ($expectedRootMarkdown -join ',')) "Root Markdown files must be only localized READMEs. Found: $($rootMarkdown -join ', ')" + +$readmeFiles = @('README.md', 'README.ru.md', 'README.zh-CN.md') +foreach ($readme in $readmeFiles) { + $text = Get-Content -LiteralPath (Join-Path $repoRoot $readme) -Raw + Test-Condition ($text.IndexOf($canonicalVersion, [StringComparison]::Ordinal) -ge 0) "$readme must mention release $canonicalVersion." + Test-Condition ($text.IndexOf('.NET 10', [StringComparison]::OrdinalIgnoreCase) -ge 0) "$readme must document .NET 10." + Test-Condition ($text.IndexOf('WinUI 3', [StringComparison]::OrdinalIgnoreCase) -ge 0) "$readme must document WinUI 3." + Test-Condition ($text.IndexOf('DNSCrypt', [StringComparison]::OrdinalIgnoreCase) -ge 0) "$readme must document DNSCrypt." + Test-Condition ($text.IndexOf('Managed', [StringComparison]::OrdinalIgnoreCase) -ge 0) "$readme must document managed routing." +} + +$applicationInfo = Get-Content -LiteralPath (Join-Path $repoRoot 'Shadowsocks.Core\ApplicationInfo.cs') -Raw +Test-Condition ($applicationInfo -match 'public const string Version = "(?\d+\.\d+\.\d+\.\d+)";') 'ApplicationInfo.Version is missing.' +Test-Condition ([Version]$Matches.version -eq [Version]"$canonicalVersion.0") 'ApplicationInfo.Version does not match Directory.Build.props.' +foreach ($manifestPath in @('Shadowsocks.WinUI\app.manifest', 'Shadowsocks.NetworkService\app.manifest')) { + $manifest = Get-XmlDocument $manifestPath + $identity = $manifest.SelectSingleNode("/*[local-name()='assembly']/*[local-name()='assemblyIdentity']") + Test-Condition ($null -ne $identity -and [Version]$identity.GetAttribute('version') -eq [Version]"$canonicalVersion.0") "$manifestPath version does not match Directory.Build.props." +} +$changes = Get-Content -LiteralPath (Join-Path $repoRoot 'CHANGES') -Raw +$escapedFourPartVersion = [regex]::Escape("$canonicalVersion.0") +Test-Condition ($changes -match "(?m)^$escapedFourPartVersion \d{4}-\d{2}-\d{2}\s*$") "CHANGES is missing the current release heading for $canonicalVersion.0." + +& (Join-Path $PSScriptRoot 'Validate-DnsCrypt.ps1') + +Write-Information "Repository structure validated for Shadowsocks Reborn $canonicalVersion." -InformationAction Continue diff --git a/packaging/upload.sh b/packaging/upload.sh deleted file mode 100755 index 82f48b06b..000000000 --- a/packaging/upload.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -version=$1 - -rsync --progress -e ssh shadowsocks-csharp/bin/x86/Release/Shadowsocks-win-dotnet4.0-$1.zip frs.sourceforge.net:/home/frs/project/shadowsocksgui/dist/ -rsync --progress -e ssh shadowsocks-csharp/bin/x86/Release/Shadowsocks-win-$1.zip frs.sourceforge.net:/home/frs/project/shadowsocksgui/dist/ diff --git a/shadowsocks-csharp/CommandLineOption.cs b/shadowsocks-csharp/CommandLineOption.cs deleted file mode 100644 index 1ab23ec44..000000000 --- a/shadowsocks-csharp/CommandLineOption.cs +++ /dev/null @@ -1,10 +0,0 @@ -using CommandLine; - -namespace Shadowsocks -{ - public class CommandLineOption - { - [Option("open-url",Required = false,HelpText = "Add an ss:// URL")] - public string OpenUrl { get; set; } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Controller/HotkeyReg.cs b/shadowsocks-csharp/Controller/HotkeyReg.cs deleted file mode 100644 index 25b1dd933..000000000 --- a/shadowsocks-csharp/Controller/HotkeyReg.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Windows.Forms; -using NLog; -using Shadowsocks.Controller.Hotkeys; -using Shadowsocks.Model; - -namespace Shadowsocks.Controller -{ - static class HotkeyReg - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - public static void RegAllHotkeys() - { - var hotkeyConfig = Program.MainController.GetCurrentConfiguration().hotkey; - - if (hotkeyConfig == null || !hotkeyConfig.RegHotkeysAtStartup) - return; - - // if any of the hotkey reg fail, undo everything - if (RegHotkeyFromString(hotkeyConfig.SwitchSystemProxy, "SwitchSystemProxyCallback") - && RegHotkeyFromString(hotkeyConfig.SwitchSystemProxyMode, "SwitchSystemProxyModeCallback") - && RegHotkeyFromString(hotkeyConfig.SwitchAllowLan, "SwitchAllowLanCallback") - && RegHotkeyFromString(hotkeyConfig.ShowLogs, "ShowLogsCallback") - && RegHotkeyFromString(hotkeyConfig.ServerMoveUp, "ServerMoveUpCallback") - && RegHotkeyFromString(hotkeyConfig.ServerMoveDown, "ServerMoveDownCallback") - ) - { - // success - } - else - { - RegHotkeyFromString("", "SwitchSystemProxyCallback"); - RegHotkeyFromString("", "SwitchSystemProxyModeCallback"); - RegHotkeyFromString("", "SwitchAllowLanCallback"); - RegHotkeyFromString("", "ShowLogsCallback"); - RegHotkeyFromString("", "ServerMoveUpCallback"); - RegHotkeyFromString("", "ServerMoveDownCallback"); - MessageBox.Show(I18N.GetString("Register hotkey failed"), I18N.GetString("Shadowsocks")); - } - } - - public static bool RegHotkeyFromString(string hotkeyStr, string callbackName, Action onComplete = null) - { - var _callback = HotkeyCallbacks.GetCallback(callbackName); - if (_callback == null) - { - throw new Exception($"{callbackName} not found"); - } - - var callback = _callback as HotKeys.HotKeyCallBackHandler; - - if (string.IsNullOrEmpty(hotkeyStr)) - { - HotKeys.UnregExistingHotkey(callback); - onComplete?.Invoke(RegResult.UnregSuccess); - return true; - } - else - { - var hotkey = HotKeys.Str2HotKey(hotkeyStr); - if (hotkey == null) - { - logger.Error($"Cannot parse hotkey: {hotkeyStr}"); - onComplete?.Invoke(RegResult.ParseError); - return false; - } - else - { - bool regResult = (HotKeys.RegHotkey(hotkey, callback)); - if (regResult) - { - onComplete?.Invoke(RegResult.RegSuccess); - } - else - { - onComplete?.Invoke(RegResult.RegFailure); - } - return regResult; - } - } - } - - public enum RegResult - { - RegSuccess, - RegFailure, - ParseError, - UnregSuccess, - //UnregFailure - } - } -} diff --git a/shadowsocks-csharp/Controller/I18N.cs b/shadowsocks-csharp/Controller/I18N.cs deleted file mode 100755 index eb948c39e..000000000 --- a/shadowsocks-csharp/Controller/I18N.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Microsoft.VisualBasic.FileIO; -using NLog; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using System.Windows.Forms; - -namespace Shadowsocks.Controller -{ - public static class I18N - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public const string I18N_FILE = "i18n.csv"; - - private static Dictionary _strings = new Dictionary(); - - private static void Init(string res, string locale) - { - using (TextFieldParser csvParser = new TextFieldParser(new StringReader(res))) - { - csvParser.SetDelimiters(","); - - // search language index - string[] localeNames = csvParser.ReadFields(); - - int enIndex = 0; - int targetIndex = -1; - - for (int i = 0; i < localeNames.Length; i++) - { - if (localeNames[i] == "en") - enIndex = i; - if (localeNames[i] == locale) - targetIndex = i; - } - - // Fallback to same language with different region - if (targetIndex == -1) - { - string localeNoRegion = locale.Split('-')[0]; - for (int i = 0; i < localeNames.Length; i++) - { - if (localeNames[i].Split('-')[0] == localeNoRegion) - targetIndex = i; - } - if (targetIndex != -1 && enIndex != targetIndex) - { - logger.Info($"Using {localeNames[targetIndex]} translation for {locale}"); - } - else - { - // Still not found, exit - logger.Info($"Translation for {locale} not found"); - return; - } - } - - // read translation lines - while (!csvParser.EndOfData) - { - string[] translations = csvParser.ReadFields(); - string source = translations[enIndex]; - string translation = translations[targetIndex]; - - // source string or translation empty - if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(translation)) continue; - // line start with comment - if (translations[0].TrimStart(' ')[0] == '#') continue; - - _strings[source] = translation; - } - } - } - - static I18N() - { - string i18n; - string locale = CultureInfo.CurrentCulture.Name; - if (!File.Exists(I18N_FILE)) - { - i18n = Resources.i18n_csv; - //File.WriteAllText(I18N_FILE, i18n, Encoding.UTF8); - } - else - { - logger.Info("Using external translation"); - i18n = File.ReadAllText(I18N_FILE, Encoding.UTF8); - } - logger.Info("Current language is: " + locale); - Init(i18n, locale); - } - - public static string GetString(string key, params object[] args) - { - return string.Format(_strings.TryGetValue(key.Trim(), out var value) ? value : key, args); - } - - public static void TranslateForm(Form c) - { - if (c == null) return; - c.Text = GetString(c.Text); - foreach (var item in ViewUtils.GetChildControls(c)) - { - if (item == null) continue; - item.Text = GetString(item.Text); - } - TranslateMenu(c.Menu); - } - public static void TranslateMenu(Menu m) - { - if (m == null) return; - foreach (var item in ViewUtils.GetMenuItems(m)) - { - if (item == null) continue; - item.Text = GetString(item.Text); - } - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/GeositeUpdater.cs b/shadowsocks-csharp/Controller/Service/GeositeUpdater.cs deleted file mode 100644 index 181605dc9..000000000 --- a/shadowsocks-csharp/Controller/Service/GeositeUpdater.cs +++ /dev/null @@ -1,361 +0,0 @@ -using NLog; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Shadowsocks.Model; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using System.Security.Cryptography; - -namespace Shadowsocks.Controller -{ - public class GeositeResultEventArgs : EventArgs - { - public bool Success; - - public GeositeResultEventArgs(bool success) - { - this.Success = success; - } - } - - public static class GeositeUpdater - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public static event EventHandler UpdateCompleted; - - public static event ErrorEventHandler Error; - - private static readonly string DATABASE_PATH = Utils.GetTempPath("dlc.dat"); - - private static readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat"; - private static readonly string GEOSITE_SHA256SUM_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat.sha256sum"; - private static byte[] geositeDB; - - public static readonly Dictionary> Geosites = new Dictionary>(); - - static GeositeUpdater() - { - if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0) - { - geositeDB = File.ReadAllBytes(DATABASE_PATH); - } - else - { - geositeDB = Resources.dlc_dat; - File.WriteAllBytes(DATABASE_PATH, Resources.dlc_dat); - } - LoadGeositeList(); - } - - /// - /// load new GeoSite data from geositeDB - /// - static void LoadGeositeList() - { - var list = GeositeList.Parser.ParseFrom(geositeDB); - foreach (var item in list.Entries) - { - Geosites[item.GroupName.ToLowerInvariant()] = item.Domains; - } - } - - public static void ResetEvent() - { - UpdateCompleted = null; - Error = null; - } - - public static async Task UpdatePACFromGeosite() - { - var geositeUrl = GEOSITE_URL; - var geositeSha256sumUrl = GEOSITE_SHA256SUM_URL; - var geositeVerifySha256 = true; - var geositeSha256sum = ""; - var mySHA256 = SHA256.Create(); - var config = Program.MainController.GetCurrentConfiguration(); - var blacklist = config.geositePreferDirect; - var httpClient = Program.MainController.GetHttpClient(); - - if (!string.IsNullOrWhiteSpace(config.geositeUrl)) - { - logger.Info("Found custom Geosite URL in config file"); - geositeUrl = config.geositeUrl; - geositeSha256sumUrl = config.geositeSha256sumUrl; - if (string.IsNullOrWhiteSpace(geositeSha256sumUrl)) - { - geositeVerifySha256 = false; - logger.Info("Geosite SHA256 verification is disabled."); - } - } - logger.Info($"Checking Geosite from {geositeUrl}"); - - try - { - // Use sha256sum to check if local database is already latest. - if (geositeVerifySha256) - { - // download checksum first - geositeSha256sum = await httpClient.GetStringAsync(geositeSha256sumUrl); - geositeSha256sum = geositeSha256sum.Substring(0, 64).ToUpper(); - logger.Info($"Got Sha256sum: {geositeSha256sum}"); - // compare downloaded checksum with local geositeDB - byte[] localDBHashBytes = mySHA256.ComputeHash(geositeDB); - string localDBHash = BitConverter.ToString(localDBHashBytes).Replace("-", String.Empty); - logger.Info($"Local Sha256sum: {localDBHash}"); - // if already latest - if (geositeSha256sum == localDBHash) - { - logger.Info("Local GeoSite DB is up to date."); - UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(false)); - return; - } - } - - // not latest. download new DB - var downloadedBytes = await httpClient.GetByteArrayAsync(geositeUrl); - - // verify sha256sum - if (geositeVerifySha256) - { - byte[] downloadedDBHashBytes = mySHA256.ComputeHash(downloadedBytes); - string downloadedDBHash = BitConverter.ToString(downloadedDBHashBytes).Replace("-", String.Empty); - logger.Info($"Actual Sha256sum: {downloadedDBHash}"); - if (geositeSha256sum != downloadedDBHash) - { - logger.Info("Sha256sum Verification: FAILED. Downloaded GeoSite DB is corrupted. Aborting the update."); - throw new Exception("Sha256sum mismatch"); - } - else - { - logger.Info("Sha256sum Verification: PASSED. Applying to local GeoSite DB."); - } - } - - // write to geosite file - using (FileStream geositeFileStream = File.Create(DATABASE_PATH)) - await geositeFileStream.WriteAsync(downloadedBytes, 0, downloadedBytes.Length); - - // update stuff - geositeDB = downloadedBytes; - LoadGeositeList(); - bool pacFileChanged = MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, blacklist); - UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged)); - } - catch (Exception ex) - { - Error?.Invoke(null, new ErrorEventArgs(ex)); - } - } - - /// - /// Merge and write pac.txt from geosite. - /// Used at multiple places. - /// - /// A list of geosite groups configured for direct connection. - /// A list of geosite groups configured for proxied connection. - /// Whether to use blacklist mode. False for whitelist. - /// - public static bool MergeAndWritePACFile(List directGroups, List proxiedGroups, bool blacklist) - { - string abpContent = MergePACFile(directGroups, proxiedGroups, blacklist); - if (File.Exists(PACDaemon.PAC_FILE)) - { - string original = FileManager.NonExclusiveReadAllText(PACDaemon.PAC_FILE, Encoding.UTF8); - if (original == abpContent) - { - return false; - } - } - File.WriteAllText(PACDaemon.PAC_FILE, abpContent, Encoding.UTF8); - return true; - } - - /// - /// Checks if the specified group exists in GeoSite database. - /// - /// The group name to check for. - /// True if the group exists. False if the group doesn't exist. - public static bool CheckGeositeGroup(string group) => SeparateAttributeFromGroupName(group, out string groupName, out _) && Geosites.ContainsKey(groupName); - - /// - /// Separates the attribute (e.g. @cn) from a group name. - /// No checks are performed. - /// - /// A group name potentially with a trailing attribute. - /// The group name with the attribute stripped. - /// The attribute. - /// True for success. False for more than one '@'. - private static bool SeparateAttributeFromGroupName(string group, out string groupName, out string attribute) - { - var splitGroupAttributeList = group.Split('@'); - if (splitGroupAttributeList.Length == 1) // no attribute - { - groupName = splitGroupAttributeList[0]; - attribute = ""; - } - else if (splitGroupAttributeList.Length == 2) // has attribute - { - groupName = splitGroupAttributeList[0]; - attribute = splitGroupAttributeList[1]; - } - else - { - groupName = ""; - attribute = ""; - return false; - } - return true; - } - - private static string MergePACFile(List directGroups, List proxiedGroups, bool blacklist) - { - string abpContent; - if (File.Exists(PACDaemon.USER_ABP_FILE)) - { - abpContent = FileManager.NonExclusiveReadAllText(PACDaemon.USER_ABP_FILE, Encoding.UTF8); - } - else - { - abpContent = Resources.abp_js; - } - - List userruleLines = new List(); - if (File.Exists(PACDaemon.USER_RULE_FILE)) - { - string userrulesString = FileManager.NonExclusiveReadAllText(PACDaemon.USER_RULE_FILE, Encoding.UTF8); - userruleLines = ProcessUserRules(userrulesString); - } - - List ruleLines = GenerateRules(directGroups, proxiedGroups, blacklist); - abpContent = -$@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)}; -var __RULES__ = {JsonConvert.SerializeObject(ruleLines, Formatting.Indented)}; -{abpContent}"; - return abpContent; - } - - private static List ProcessUserRules(string content) - { - List valid_lines = new List(); - using (var stringReader = new StringReader(content)) - { - for (string line = stringReader.ReadLine(); line != null; line = stringReader.ReadLine()) - { - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("!") || line.StartsWith("[")) - continue; - valid_lines.Add(line); - } - } - return valid_lines; - } - - /// - /// Generates rule lines based on user preference. - /// - /// A list of geosite groups configured for direct connection. - /// A list of geosite groups configured for proxied connection. - /// Whether to use blacklist mode. False for whitelist. - /// A list of rule lines. - private static List GenerateRules(List directGroups, List proxiedGroups, bool blacklist) - { - List ruleLines; - if (blacklist) // blocking + exception rules - { - ruleLines = GenerateBlockingRules(proxiedGroups); - ruleLines.AddRange(GenerateExceptionRules(directGroups)); - } - else // proxy all + exception rules - { - ruleLines = new List() - { - "/.*/" // block/proxy all unmatched domains - }; - ruleLines.AddRange(GenerateExceptionRules(directGroups)); - } - return ruleLines; - } - - /// - /// Generates rules that match domains that should be proxied. - /// - /// A list of source groups. - /// A list of rule lines. - private static List GenerateBlockingRules(List groups) - { - List ruleLines = new List(); - foreach (var group in groups) - { - // separate group name and attribute - SeparateAttributeFromGroupName(group, out string groupName, out string attribute); - var domainObjects = Geosites[groupName]; - if (!string.IsNullOrEmpty(attribute)) // has attribute - { - var attributeObject = new DomainObject.Types.Attribute - { - Key = attribute, - BoolValue = true - }; - foreach (var domainObject in domainObjects) - { - if (domainObject.Attribute.Contains(attributeObject)) - switch (domainObject.Type) - { - case DomainObject.Types.Type.Plain: - ruleLines.Add(domainObject.Value); - break; - case DomainObject.Types.Type.Regex: - ruleLines.Add($"/{domainObject.Value}/"); - break; - case DomainObject.Types.Type.Domain: - ruleLines.Add($"||{domainObject.Value}"); - break; - case DomainObject.Types.Type.Full: - ruleLines.Add($"|http://{domainObject.Value}"); - ruleLines.Add($"|https://{domainObject.Value}"); - break; - } - } - } - else // no attribute - foreach (var domainObject in domainObjects) - { - switch (domainObject.Type) - { - case DomainObject.Types.Type.Plain: - ruleLines.Add(domainObject.Value); - break; - case DomainObject.Types.Type.Regex: - ruleLines.Add($"/{domainObject.Value}/"); - break; - case DomainObject.Types.Type.Domain: - ruleLines.Add($"||{domainObject.Value}"); - break; - case DomainObject.Types.Type.Full: - ruleLines.Add($"|http://{domainObject.Value}"); - ruleLines.Add($"|https://{domainObject.Value}"); - break; - } - } - } - return ruleLines; - } - - /// - /// Generates rules that match domains that should be connected directly without a proxy. - /// - /// A list of source groups. - /// A list of rule lines. - private static List GenerateExceptionRules(List groups) - => GenerateBlockingRules(groups) - .Select(r => $"@@{r}") // convert blocking rules to exception rules - .ToList(); - } -} diff --git a/shadowsocks-csharp/Controller/Service/IPCService.cs b/shadowsocks-csharp/Controller/Service/IPCService.cs deleted file mode 100644 index 5e32f4aa2..000000000 --- a/shadowsocks-csharp/Controller/Service/IPCService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.IO.Pipes; -using System.Net; -using System.Text; - -namespace Shadowsocks.Controller -{ - class RequestAddUrlEventArgs : EventArgs - { - public readonly string Url; - - public RequestAddUrlEventArgs(string url) - { - this.Url = url; - } - } - - internal class IPCService - { - private const int INT32_LEN = 4; - private const int OP_OPEN_URL = 1; - private static readonly string PIPE_PATH = $"Shadowsocks\\{Program.ExecutablePath.GetHashCode()}"; - - public event EventHandler OpenUrlRequested; - - public async void RunServer() - { - byte[] buf = new byte[4096]; - while (true) - { - using (NamedPipeServerStream stream = new NamedPipeServerStream(PIPE_PATH)) - { - await stream.WaitForConnectionAsync(); - await stream.ReadAsync(buf, 0, INT32_LEN); - int opcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0)); - if (opcode == OP_OPEN_URL) - { - await stream.ReadAsync(buf, 0, INT32_LEN); - int strlen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0)); - - await stream.ReadAsync(buf, 0, strlen); - string url = Encoding.UTF8.GetString(buf, 0, strlen); - - OpenUrlRequested?.Invoke(this, new RequestAddUrlEventArgs(url)); - } - stream.Close(); - } - } - } - - private static (NamedPipeClientStream, bool) TryConnect() - { - NamedPipeClientStream pipe = new NamedPipeClientStream(PIPE_PATH); - bool exist; - try - { - pipe.Connect(10); - exist = true; - } - catch (TimeoutException) - { - exist = false; - } - return (pipe, exist); - } - - public static bool AnotherInstanceRunning() - { - (NamedPipeClientStream pipe, bool exist) = TryConnect(); - pipe.Dispose(); - return exist; - } - - public static void RequestOpenUrl(string url) - { - (NamedPipeClientStream pipe, bool exist) = TryConnect(); - if(!exist) return; - byte[] opAddUrl = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(OP_OPEN_URL)); - pipe.Write(opAddUrl, 0, INT32_LEN); // opcode addurl - byte[] b = Encoding.UTF8.GetBytes(url); - byte[] blen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(b.Length)); - pipe.Write(blen, 0, INT32_LEN); - pipe.Write(b, 0, b.Length); - pipe.Close(); - pipe.Dispose(); - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/Listener.cs b/shadowsocks-csharp/Controller/Service/Listener.cs deleted file mode 100644 index 2627587cd..000000000 --- a/shadowsocks-csharp/Controller/Service/Listener.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using NLog; -using Shadowsocks.Model; - -namespace Shadowsocks.Controller -{ - public class Listener - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public interface IService - { - bool Handle(byte[] firstPacket, int length, Socket socket, object state); - - void Stop(); - } - - public abstract class Service : IService - { - public abstract bool Handle(byte[] firstPacket, int length, Socket socket, object state); - - public virtual void Stop() { } - } - - public class UDPState - { - public UDPState(Socket s) - { - socket = s; - remoteEndPoint = new IPEndPoint(s.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, 0); - } - public Socket socket; - public byte[] buffer = new byte[4096]; - public EndPoint remoteEndPoint; - } - - Configuration _config; - bool _shareOverLAN; - Socket _tcpSocket; - Socket _udpSocket; - List _services; - - public Listener(List services) - { - this._services = services; - } - - private bool CheckIfPortInUse(int port) - { - IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); - return ipProperties.GetActiveTcpListeners().Any(endPoint => endPoint.Port == port); - } - - public void Start(Configuration config) - { - this._config = config; - this._shareOverLAN = config.shareOverLan; - - if (CheckIfPortInUse(_config.localPort)) - throw new Exception(I18N.GetString("Port {0} already in use", _config.localPort)); - - try - { - // Create a TCP/IP socket. - _tcpSocket = new Socket(config.isIPv6Enabled ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - _udpSocket = new Socket(config.isIPv6Enabled ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - _tcpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - IPEndPoint localEndPoint = null; - localEndPoint = _shareOverLAN - ? new IPEndPoint(config.isIPv6Enabled ? IPAddress.IPv6Any : IPAddress.Any, _config.localPort) - : new IPEndPoint(config.isIPv6Enabled ? IPAddress.IPv6Loopback : IPAddress.Loopback, _config.localPort); - - // Bind the socket to the local endpoint and listen for incoming connections. - _tcpSocket.Bind(localEndPoint); - _udpSocket.Bind(localEndPoint); - _tcpSocket.Listen(1024); - - // Start an asynchronous socket to listen for connections. - logger.Info($"Shadowsocks started ({UpdateChecker.Version})"); - logger.Debug(Encryption.EncryptorFactory.DumpRegisteredEncryptor()); - _tcpSocket.BeginAccept(new AsyncCallback(AcceptCallback), _tcpSocket); - UDPState udpState = new UDPState(_udpSocket); - _udpSocket.BeginReceiveFrom(udpState.buffer, 0, udpState.buffer.Length, 0, ref udpState.remoteEndPoint, new AsyncCallback(RecvFromCallback), udpState); - } - catch (SocketException) - { - _tcpSocket.Close(); - throw; - } - } - - public void Stop() - { - if (_tcpSocket != null) - { - _tcpSocket.Close(); - _tcpSocket = null; - } - if (_udpSocket != null) - { - _udpSocket.Close(); - _udpSocket = null; - } - - _services.ForEach(s => s.Stop()); - } - - public void RecvFromCallback(IAsyncResult ar) - { - UDPState state = (UDPState)ar.AsyncState; - var socket = state.socket; - try - { - int bytesRead = socket.EndReceiveFrom(ar, ref state.remoteEndPoint); - foreach (IService service in _services) - { - if (service.Handle(state.buffer, bytesRead, socket, state)) - { - break; - } - } - } - catch (ObjectDisposedException) - { - } - catch (Exception ex) - { - logger.Debug(ex); - } - finally - { - try - { - socket.BeginReceiveFrom(state.buffer, 0, state.buffer.Length, 0, ref state.remoteEndPoint, new AsyncCallback(RecvFromCallback), state); - } - catch (ObjectDisposedException) - { - // do nothing - } - catch (Exception) - { - } - } - } - - public void AcceptCallback(IAsyncResult ar) - { - Socket listener = (Socket)ar.AsyncState; - try - { - Socket conn = listener.EndAccept(ar); - - byte[] buf = new byte[4096]; - object[] state = new object[] { - conn, - buf - }; - - conn.BeginReceive(buf, 0, buf.Length, 0, - new AsyncCallback(ReceiveCallback), state); - } - catch (ObjectDisposedException) - { - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - finally - { - try - { - listener.BeginAccept( - new AsyncCallback(AcceptCallback), - listener); - } - catch (ObjectDisposedException) - { - // do nothing - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - } - } - - private void ReceiveCallback(IAsyncResult ar) - { - object[] state = (object[])ar.AsyncState; - - Socket conn = (Socket)state[0]; - byte[] buf = (byte[])state[1]; - try - { - int bytesRead = conn.EndReceive(ar); - if (bytesRead <= 0) goto Shutdown; - foreach (IService service in _services) - { - if (service.Handle(buf, bytesRead, conn, null)) - { - return; - } - } - Shutdown: - // no service found for this - if (conn.ProtocolType == ProtocolType.Tcp) - { - conn.Close(); - } - } - catch (Exception e) - { - logger.LogUsefulException(e); - conn.Close(); - } - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/PACDaemon.cs b/shadowsocks-csharp/Controller/Service/PACDaemon.cs deleted file mode 100644 index 217f0fda9..000000000 --- a/shadowsocks-csharp/Controller/Service/PACDaemon.cs +++ /dev/null @@ -1,133 +0,0 @@ -using NLog; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shadowsocks.Controller -{ - - /// - /// Processing the PAC file content - /// - public class PACDaemon - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public const string PAC_FILE = "pac.txt"; - public const string USER_RULE_FILE = "user-rule.txt"; - public const string USER_ABP_FILE = "abp.txt"; - private Configuration config; - - FileSystemWatcher PACFileWatcher; - FileSystemWatcher UserRuleFileWatcher; - - public event EventHandler PACFileChanged; - public event EventHandler UserRuleFileChanged; - - public PACDaemon(Configuration config) - { - this.config = config; - TouchPACFile(); - TouchUserRuleFile(); - - this.WatchPacFile(); - this.WatchUserRuleFile(); - } - - - public string TouchPACFile() - { - if (!File.Exists(PAC_FILE)) - { - GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); - } - return PAC_FILE; - } - - internal string TouchUserRuleFile() - { - if (!File.Exists(USER_RULE_FILE)) - { - File.WriteAllText(USER_RULE_FILE, Resources.user_rule); - } - return USER_RULE_FILE; - } - - internal string GetPACContent() - { - if (!File.Exists(PAC_FILE)) - { - GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); - } - return File.ReadAllText(PAC_FILE, Encoding.UTF8); - } - - - private void WatchPacFile() - { - PACFileWatcher?.Dispose(); - PACFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); - PACFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; - PACFileWatcher.Filter = PAC_FILE; - PACFileWatcher.Changed += PACFileWatcher_Changed; - PACFileWatcher.Created += PACFileWatcher_Changed; - PACFileWatcher.Deleted += PACFileWatcher_Changed; - PACFileWatcher.Renamed += PACFileWatcher_Changed; - PACFileWatcher.EnableRaisingEvents = true; - } - - private void WatchUserRuleFile() - { - UserRuleFileWatcher?.Dispose(); - UserRuleFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); - UserRuleFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; - UserRuleFileWatcher.Filter = USER_RULE_FILE; - UserRuleFileWatcher.Changed += UserRuleFileWatcher_Changed; - UserRuleFileWatcher.Created += UserRuleFileWatcher_Changed; - UserRuleFileWatcher.Deleted += UserRuleFileWatcher_Changed; - UserRuleFileWatcher.Renamed += UserRuleFileWatcher_Changed; - UserRuleFileWatcher.EnableRaisingEvents = true; - } - - #region FileSystemWatcher.OnChanged() - // FileSystemWatcher Changed event is raised twice - // http://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice - // Add a short delay to avoid raise event twice in a short period - private void PACFileWatcher_Changed(object sender, FileSystemEventArgs e) - { - if (PACFileChanged != null) - { - logger.Info($"Detected: PAC file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); - Task.Factory.StartNew(() => - { - ((FileSystemWatcher)sender).EnableRaisingEvents = false; - System.Threading.Thread.Sleep(10); - PACFileChanged(this, new EventArgs()); - ((FileSystemWatcher)sender).EnableRaisingEvents = true; - }); - } - } - - private void UserRuleFileWatcher_Changed(object sender, FileSystemEventArgs e) - { - if (UserRuleFileChanged != null) - { - logger.Info($"Detected: User Rule file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); - Task.Factory.StartNew(() => - { - ((FileSystemWatcher)sender).EnableRaisingEvents = false; - System.Threading.Thread.Sleep(10); - UserRuleFileChanged(this, new EventArgs()); - ((FileSystemWatcher)sender).EnableRaisingEvents = true; - }); - } - } - #endregion - } -} diff --git a/shadowsocks-csharp/Controller/Service/PACServer.cs b/shadowsocks-csharp/Controller/Service/PACServer.cs deleted file mode 100644 index 5a84fe8e9..000000000 --- a/shadowsocks-csharp/Controller/Service/PACServer.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Shadowsocks.Encryption; -using Shadowsocks.Model; -using Shadowsocks.Util; -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Web; -using NLog; - -namespace Shadowsocks.Controller -{ - public class PACServer : Listener.Service - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public const string RESOURCE_NAME = "pac"; - - private string PacSecret - { - get - { - if (string.IsNullOrEmpty(_cachedPacSecret)) - { - var rd = new byte[32]; - RNG.GetBytes(rd); - _cachedPacSecret = HttpServerUtility.UrlTokenEncode(rd); - } - return _cachedPacSecret; - } - } - private string _cachedPacSecret = ""; - public string PacUrl { get; private set; } = ""; - - private Configuration _config; - private PACDaemon _pacDaemon; - - public PACServer(PACDaemon pacDaemon) - { - _pacDaemon = pacDaemon; - } - - public void UpdatePACURL(Configuration config) - { - _config = config; - string usedSecret = _config.secureLocalPac ? $"&secret={PacSecret}" : ""; - string contentHash = GetHash(_pacDaemon.GetPACContent()); - PacUrl = $"http://{config.LocalHost}:{config.localPort}/{RESOURCE_NAME}?hash={contentHash}{usedSecret}"; - logger.Debug("Set PAC URL:" + PacUrl); - } - - private static string GetHash(string content) - { - return HttpServerUtility.UrlTokenEncode(MbedTLS.MD5(Encoding.ASCII.GetBytes(content))); - } - - public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) - { - if (socket.ProtocolType != ProtocolType.Tcp) - { - return false; - } - - try - { - /* - * RFC 7230 - * - GET /hello.txt HTTP/1.1 - User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 - Host: www.example.com - Accept-Language: en, mi - */ - - string request = Encoding.UTF8.GetString(firstPacket, 0, length); - string[] lines = request.Split('\r', '\n'); - bool hostMatch = false, pathMatch = false, useSocks = false; - bool secretMatch = !_config.secureLocalPac; - - if (lines.Length < 2) // need at lease RequestLine + Host - { - return false; - } - - // parse request line - string requestLine = lines[0]; - // GET /pac?t=yyyyMMddHHmmssfff&secret=foobar HTTP/1.1 - string[] requestItems = requestLine.Split(' '); - if (requestItems.Length == 3 && requestItems[0] == "GET") - { - int index = requestItems[1].IndexOf('?'); - if (index < 0) - { - index = requestItems[1].Length; - } - string resourceString = requestItems[1].Substring(0, index).Remove(0, 1); - if (string.Equals(resourceString, RESOURCE_NAME, StringComparison.OrdinalIgnoreCase)) - { - pathMatch = true; - if (!secretMatch) - { - string queryString = requestItems[1].Substring(index); - if (queryString.Contains(PacSecret)) - { - secretMatch = true; - } - } - } - } - - // parse request header - for (int i = 1; i < lines.Length; i++) - { - if (string.IsNullOrEmpty(lines[i])) - continue; - - string[] kv = lines[i].Split(new char[] { ':' }, 2); - if (kv.Length == 2) - { - if (kv[0] == "Host") - { - if (kv[1].Trim() == ((IPEndPoint)socket.LocalEndPoint).ToString()) - { - hostMatch = true; - } - } - //else if (kv[0] == "User-Agent") - //{ - // // we need to drop connections when changing servers - // if (kv[1].IndexOf("Chrome") >= 0) - // { - // useSocks = true; - // } - //} - } - } - - if (hostMatch && pathMatch) - { - if (!secretMatch) - { - socket.Close(); // Close immediately - } - else - { - SendResponse(socket, useSocks); - } - return true; - } - return false; - } - catch (ArgumentException) - { - return false; - } - } - - - - public void SendResponse(Socket socket, bool useSocks) - { - try - { - IPEndPoint localEndPoint = (IPEndPoint)socket.LocalEndPoint; - - string proxy = GetPACAddress(localEndPoint, useSocks); - - string pacContent = $"var __PROXY__ = '{proxy}';\n" + _pacDaemon.GetPACContent(); - string responseHead = -$@"HTTP/1.1 200 OK -Server: ShadowsocksWindows/{UpdateChecker.Version} -Content-Type: application/x-ns-proxy-autoconfig -Content-Length: { Encoding.UTF8.GetBytes(pacContent).Length} -Connection: Close - -"; - byte[] response = Encoding.UTF8.GetBytes(responseHead + pacContent); - socket.BeginSend(response, 0, response.Length, 0, new AsyncCallback(SendCallback), socket); - } - catch (Exception e) - { - logger.LogUsefulException(e); - socket.Close(); - } - } - - private void SendCallback(IAsyncResult ar) - { - Socket conn = (Socket)ar.AsyncState; - try - { - conn.Shutdown(SocketShutdown.Send); - } - catch - { } - } - - - private string GetPACAddress(IPEndPoint localEndPoint, bool useSocks) - { - return localEndPoint.AddressFamily == AddressFamily.InterNetworkV6 - ? $"{(useSocks ? "SOCKS5" : "PROXY")} [{localEndPoint.Address}]:{_config.localPort};" - : $"{(useSocks ? "SOCKS5" : "PROXY")} {localEndPoint.Address}:{_config.localPort};"; - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/PortForwarder.cs b/shadowsocks-csharp/Controller/Service/PortForwarder.cs deleted file mode 100644 index 8284fd11d..000000000 --- a/shadowsocks-csharp/Controller/Service/PortForwarder.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using NLog; -using Shadowsocks.Util.Sockets; - -namespace Shadowsocks.Controller -{ - class PortForwarder : Listener.Service - { - private readonly int _targetPort; - - public PortForwarder(int targetPort) - { - _targetPort = targetPort; - } - - public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) - { - if (socket.ProtocolType != ProtocolType.Tcp) - { - return false; - } - new Handler().Start(firstPacket, length, socket, _targetPort); - return true; - } - - private class Handler - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private byte[] _firstPacket; - private int _firstPacketLength; - private Socket _local; - private WrappedSocket _remote; - private bool _closed = false; - private bool _localShutdown = false; - private bool _remoteShutdown = false; - private const int RecvSize = 2048; - // remote receive buffer - private byte[] remoteRecvBuffer = new byte[RecvSize]; - // connection receive buffer - private byte[] connetionRecvBuffer = new byte[RecvSize]; - - // instance-based lock - private readonly object _Lock = new object(); - - public void Start(byte[] firstPacket, int length, Socket socket, int targetPort) - { - _firstPacket = firstPacket; - _firstPacketLength = length; - _local = socket; - try - { - // Local Port Forward use IP as is - EndPoint remoteEP = SocketUtil.GetEndPoint(_local.AddressFamily == AddressFamily.InterNetworkV6 ? "[::1]" : "127.0.0.1", targetPort); - - // Connect to the remote endpoint. - _remote = new WrappedSocket(); - _remote.BeginConnect(remoteEP, ConnectCallback, null); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void ConnectCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - _remote.EndConnect(ar); - _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - HandshakeReceive(); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void HandshakeReceive() - { - if (_closed) - { - return; - } - try - { - _remote.BeginSend(_firstPacket, 0, _firstPacketLength, 0, StartPipe, null); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void StartPipe(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - _remote.EndSend(ar); - _remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, - PipeRemoteReceiveCallback, null); - _local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, - PipeConnectionReceiveCallback, null); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void PipeRemoteReceiveCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - int bytesRead = _remote.EndReceive(ar); - if (bytesRead > 0) - { - _local.BeginSend(remoteRecvBuffer, 0, bytesRead, 0, PipeConnectionSendCallback, null); - } - else - { - _local.Shutdown(SocketShutdown.Send); - _localShutdown = true; - CheckClose(); - } - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void PipeConnectionReceiveCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - int bytesRead = _local.EndReceive(ar); - if (bytesRead > 0) - { - _remote.BeginSend(connetionRecvBuffer, 0, bytesRead, 0, PipeRemoteSendCallback, null); - } - else - { - _remote.Shutdown(SocketShutdown.Send); - _remoteShutdown = true; - CheckClose(); - } - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void PipeRemoteSendCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - _remote.EndSend(ar); - _local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, - PipeConnectionReceiveCallback, null); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void PipeConnectionSendCallback(IAsyncResult ar) - { - if (_closed) - { - return; - } - try - { - _local.EndSend(ar); - _remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, - PipeRemoteReceiveCallback, null); - } - catch (Exception e) - { - logger.LogUsefulException(e); - Close(); - } - } - - private void CheckClose() - { - if (_localShutdown && _remoteShutdown) - { - Close(); - } - } - - public void Close() - { - lock (_Lock) - { - if (_closed) - { - return; - } - _closed = true; - } - if (_local != null) - { - try - { - _local.Shutdown(SocketShutdown.Both); - _local.Close(); - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - } - if (_remote != null) - { - try - { - _remote.Shutdown(SocketShutdown.Both); - _remote.Dispose(); - } - catch (SocketException e) - { - logger.LogUsefulException(e); - } - } - } - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/PrivoxyRunner.cs b/shadowsocks-csharp/Controller/Service/PrivoxyRunner.cs deleted file mode 100644 index 1a7aaebf9..000000000 --- a/shadowsocks-csharp/Controller/Service/PrivoxyRunner.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Windows.Forms; -using NLog; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using Shadowsocks.Util.ProcessManagement; - -namespace Shadowsocks.Controller -{ - class PrivoxyRunner - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private static int _uid; - private static string _uniqueConfigFile; - private static Job _privoxyJob; - private Process _process; - private int _runningPort; - - static PrivoxyRunner() - { - try - { - _uid = Program.WorkingDirectory.GetHashCode(); // Currently we use ss's StartupPath to identify different Privoxy instance. - _uniqueConfigFile = $"privoxy_{_uid}.conf"; - _privoxyJob = new Job(); - - FileManager.UncompressFile(Utils.GetTempPath("ss_privoxy.exe"), Resources.privoxy_exe); - } - catch (IOException e) - { - logger.LogUsefulException(e); - } - } - - public int RunningPort => _runningPort; - - public void Start(Configuration configuration) - { - if (_process == null) - { - Process[] existingPrivoxy = Process.GetProcessesByName("ss_privoxy"); - foreach (Process p in existingPrivoxy.Where(IsChildProcess)) - { - KillProcess(p); - } - string privoxyConfig = Resources.privoxy_conf; - _runningPort = GetFreePort(configuration.isIPv6Enabled); - privoxyConfig = privoxyConfig.Replace("__SOCKS_PORT__", configuration.localPort.ToString()); - privoxyConfig = privoxyConfig.Replace("__PRIVOXY_BIND_PORT__", _runningPort.ToString()); - privoxyConfig = configuration.isIPv6Enabled - ? privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "[::]" : "[::1]") - .Replace("__SOCKS_HOST__", "[::1]") - : privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "0.0.0.0" : "127.0.0.1") - .Replace("__SOCKS_HOST__", "127.0.0.1"); - FileManager.ByteArrayToFile(Utils.GetTempPath(_uniqueConfigFile), Encoding.UTF8.GetBytes(privoxyConfig)); - - _process = new Process - { - // Configure the process using the StartInfo properties. - StartInfo = - { - FileName = "ss_privoxy.exe", - Arguments = _uniqueConfigFile, - WorkingDirectory = Utils.GetTempPath(), - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = true, - CreateNoWindow = true - } - }; - _process.Start(); - - /* - * Add this process to job obj associated with this ss process, so that - * when ss exit unexpectedly, this process will be forced killed by system. - */ - _privoxyJob.AddProcess(_process.Handle); - } - } - - public void Stop() - { - if (_process != null) - { - KillProcess(_process); - _process.Dispose(); - _process = null; - } - } - - private static void KillProcess(Process p) - { - try - { - p.CloseMainWindow(); - p.WaitForExit(100); - if (!p.HasExited) - { - p.Kill(); - p.WaitForExit(); - } - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - } - - /* - * We won't like to kill other ss instances' ss_privoxy.exe. - * This function will check whether the given process is created - * by this process by checking the module path or command line. - * - * Since it's required to put ss in different dirs to run muti instances, - * different instance will create their unique "privoxy_UID.conf" where - * UID is hash of ss's location. - */ - - private static bool IsChildProcess(Process process) - { - try - { - /* - * Under PortableMode, we could identify it by the path of ss_privoxy.exe. - */ - var path = process.MainModule.FileName; - - return Utils.GetTempPath("ss_privoxy.exe").Equals(path); - - } - catch (Exception ex) - { - /* - * Sometimes Process.GetProcessesByName will return some processes that - * are already dead, and that will cause exceptions here. - * We could simply ignore those exceptions. - */ - logger.LogUsefulException(ex); - return false; - } - } - - private int GetFreePort(bool isIPv6 = false) - { - int defaultPort = 8123; - try - { - // TCP stack please do me a favor - TcpListener l = new TcpListener(isIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); - l.Start(); - var port = ((IPEndPoint)l.LocalEndpoint).Port; - l.Stop(); - return port; - } - catch (Exception e) - { - // in case access denied - logger.LogUsefulException(e); - return defaultPort; - } - } - } -} diff --git a/shadowsocks-csharp/Controller/Service/UDPRelay.cs b/shadowsocks-csharp/Controller/Service/UDPRelay.cs deleted file mode 100644 index 568e1f75e..000000000 --- a/shadowsocks-csharp/Controller/Service/UDPRelay.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using NLog; -using Shadowsocks.Controller.Strategy; -using Shadowsocks.Encryption; -using Shadowsocks.Model; - -namespace Shadowsocks.Controller -{ - class UDPRelay : Listener.Service - { - private ShadowsocksController _controller; - - // TODO: choose a smart number - private LRUCache _cache = new LRUCache(512); - - public long outbound = 0; - public long inbound = 0; - - public UDPRelay(ShadowsocksController controller) - { - this._controller = controller; - } - - public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) - { - if (socket.ProtocolType != ProtocolType.Udp) - { - return false; - } - if (length < 4) - { - return false; - } - Listener.UDPState udpState = (Listener.UDPState)state; - IPEndPoint remoteEndPoint = (IPEndPoint)udpState.remoteEndPoint; - UDPHandler handler = _cache.get(remoteEndPoint); - if (handler == null) - { - handler = new UDPHandler(socket, _controller.GetAServer(IStrategyCallerType.UDP, remoteEndPoint, null/*TODO: fix this*/), remoteEndPoint); - handler.Receive(); - _cache.add(remoteEndPoint, handler); - } - handler.Send(firstPacket, length); - return true; - } - - public class UDPHandler - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private Socket _local; - private Socket _remote; - - private Server _server; - private byte[] _buffer = new byte[65536]; - - private IPEndPoint _localEndPoint; - private IPEndPoint _remoteEndPoint; - - private IPAddress GetIPAddress() - { - switch (_remote.AddressFamily) - { - case AddressFamily.InterNetwork: - return IPAddress.Any; - case AddressFamily.InterNetworkV6: - return IPAddress.IPv6Any; - default: - return IPAddress.Any; - } - } - - public UDPHandler(Socket local, Server server, IPEndPoint localEndPoint) - { - _local = local; - _server = server; - _localEndPoint = localEndPoint; - - // TODO async resolving - IPAddress ipAddress; - bool parsed = IPAddress.TryParse(server.server, out ipAddress); - if (!parsed) - { - IPHostEntry ipHostInfo = Dns.GetHostEntry(server.server); - ipAddress = ipHostInfo.AddressList[0]; - } - _remoteEndPoint = new IPEndPoint(ipAddress, server.server_port); - _remote = new Socket(_remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); - _remote.Bind(new IPEndPoint(GetIPAddress(), 0)); - } - - public void Send(byte[] data, int length) - { - IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); - byte[] dataIn = new byte[length - 3]; - Array.Copy(data, 3, dataIn, 0, length - 3); - byte[] dataOut = new byte[65536]; // enough space for AEAD ciphers - int outlen; - encryptor.EncryptUDP(dataIn, length - 3, dataOut, out outlen); - logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); - _remote?.SendTo(dataOut, outlen, SocketFlags.None, _remoteEndPoint); - } - - public void Receive() - { - EndPoint remoteEndPoint = new IPEndPoint(GetIPAddress(), 0); - logger.Debug($"++++++Receive Server Port, size:" + _buffer.Length); - _remote?.BeginReceiveFrom(_buffer, 0, _buffer.Length, 0, ref remoteEndPoint, new AsyncCallback(RecvFromCallback), null); - } - - public void RecvFromCallback(IAsyncResult ar) - { - try - { - if (_remote == null) return; - EndPoint remoteEndPoint = new IPEndPoint(GetIPAddress(), 0); - int bytesRead = _remote.EndReceiveFrom(ar, ref remoteEndPoint); - - byte[] dataOut = new byte[bytesRead]; - int outlen; - - IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); - encryptor.DecryptUDP(_buffer, bytesRead, dataOut, out outlen); - - byte[] sendBuf = new byte[outlen + 3]; - Array.Copy(dataOut, 0, sendBuf, 3, outlen); - - logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay"); - _local?.SendTo(sendBuf, outlen + 3, 0, _localEndPoint); - - Receive(); - } - catch (ObjectDisposedException) - { - // TODO: handle the ObjectDisposedException - } - catch (Exception) - { - // TODO: need more think about handle other Exceptions, or should remove this catch(). - } - finally - { - // No matter success or failed, we keep receiving - - } - } - - public void Close() - { - try - { - _remote?.Close(); - } - catch (ObjectDisposedException) - { - // TODO: handle the ObjectDisposedException - } - catch (Exception) - { - // TODO: need more think about handle other Exceptions, or should remove this catch(). - } - } - } - } - - #region LRU cache - - // cc by-sa 3.0 http://stackoverflow.com/a/3719378/1124054 - class LRUCache where V : UDPRelay.UDPHandler - { - private int capacity; - private Dictionary>> cacheMap = new Dictionary>>(); - private LinkedList> lruList = new LinkedList>(); - - public LRUCache(int capacity) - { - this.capacity = capacity; - } - - [MethodImpl(MethodImplOptions.Synchronized)] - public V get(K key) - { - LinkedListNode> node; - if (cacheMap.TryGetValue(key, out node)) - { - V value = node.Value.value; - lruList.Remove(node); - lruList.AddLast(node); - return value; - } - return default(V); - } - - [MethodImpl(MethodImplOptions.Synchronized)] - public void add(K key, V val) - { - if (cacheMap.Count >= capacity) - { - RemoveFirst(); - } - - LRUCacheItem cacheItem = new LRUCacheItem(key, val); - LinkedListNode> node = new LinkedListNode>(cacheItem); - lruList.AddLast(node); - cacheMap.Add(key, node); - } - - private void RemoveFirst() - { - // Remove from LRUPriority - LinkedListNode> node = lruList.First; - lruList.RemoveFirst(); - - // Remove from cache - cacheMap.Remove(node.Value.key); - node.Value.value.Close(); - } - } - - class LRUCacheItem - { - public LRUCacheItem(K k, V v) - { - key = k; - value = v; - } - public K key; - public V value; - } - - #endregion -} diff --git a/shadowsocks-csharp/Controller/Service/UpdateChecker.cs b/shadowsocks-csharp/Controller/Service/UpdateChecker.cs deleted file mode 100644 index be57427b1..000000000 --- a/shadowsocks-csharp/Controller/Service/UpdateChecker.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Windows; -using Newtonsoft.Json.Linq; -using NLog; -using Shadowsocks.Localization; -using Shadowsocks.Model; -using Shadowsocks.Util; -using Shadowsocks.Views; - -namespace Shadowsocks.Controller -{ - public class UpdateChecker - { - private readonly Logger logger; - private readonly HttpClient httpClient; - - // https://developer.github.com/v3/repos/releases/ - private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; - - private Configuration _config; - private Window versionUpdatePromptWindow; - private JToken _releaseObject; - - public string NewReleaseVersion { get; private set; } - public string NewReleaseZipFilename { get; private set; } - - public event EventHandler CheckUpdateCompleted; - - public const string Version = "4.4.1.0"; - private readonly Version _version; - - public UpdateChecker() - { - logger = LogManager.GetCurrentClassLogger(); - httpClient = Program.MainController.GetHttpClient(); - _version = new Version(Version); - _config = Program.MainController.GetCurrentConfiguration(); - } - - /// - /// Checks for updates and asks the user if updates are found. - /// - /// A delay in milliseconds before checking. - /// - public async Task CheckForVersionUpdate(int millisecondsDelay = 0) - { - // delay - logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); - await Task.Delay(millisecondsDelay); - // update _config so we would know if the user checked or unchecked pre-release checks - _config = Program.MainController.GetCurrentConfiguration(); - // start - logger.Info($"Checking for version update."); - try - { - // list releases via API - var releasesListJsonString = await httpClient.GetStringAsync(UpdateURL); - // parse - var releasesJArray = JArray.Parse(releasesListJsonString); - foreach (var releaseObject in releasesJArray) - { - var releaseTagName = (string)releaseObject["tag_name"]; - var releaseVersion = new Version(releaseTagName); - if (releaseTagName == _config.skippedUpdateVersion) // finished checking - break; - if (releaseVersion.CompareTo(_version) > 0 && - (!(bool)releaseObject["prerelease"] || _config.checkPreRelease && (bool)releaseObject["prerelease"])) // selected - { - logger.Info($"Found new version {releaseTagName}."); - _releaseObject = releaseObject; - NewReleaseVersion = releaseTagName; - AskToUpdate(releaseObject); - return; - } - } - logger.Info($"No new versions found."); - CheckUpdateCompleted?.Invoke(this, new EventArgs()); - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - } - - /// - /// Opens a window to show the update's information. - /// - /// The update release object. - private void AskToUpdate(JToken releaseObject) - { - if (versionUpdatePromptWindow == null) - { - versionUpdatePromptWindow = new Window() - { - Title = LocalizationProvider.GetLocalizedValue("VersionUpdate"), - Height = 480, - Width = 640, - MinHeight = 480, - MinWidth = 640, - Content = new VersionUpdatePromptView(releaseObject) - }; - versionUpdatePromptWindow.Closed += VersionUpdatePromptWindow_Closed; - versionUpdatePromptWindow.Show(); - } - versionUpdatePromptWindow.Activate(); - } - - private void VersionUpdatePromptWindow_Closed(object sender, EventArgs e) - { - versionUpdatePromptWindow = null; - } - - /// - /// Downloads the selected update and notifies the user. - /// - /// - public async Task DoUpdate() - { - try - { - var assets = (JArray)_releaseObject["assets"]; - // download all assets - foreach (JObject asset in assets) - { - var filename = (string)asset["name"]; - var browser_download_url = (string)asset["browser_download_url"]; - var response = await httpClient.GetAsync(browser_download_url); - using (var downloadedFileStream = File.Create(Utils.GetTempPath(filename))) - await response.Content.CopyToAsync(downloadedFileStream); - logger.Info($"Downloaded {filename}."); - // store .zip filename - if (filename.EndsWith(".zip")) - NewReleaseZipFilename = filename; - } - logger.Info("Finished downloading."); - // notify user - CloseVersionUpdatePromptWindow(); - Process.Start("explorer.exe", $"/select, \"{Utils.GetTempPath(NewReleaseZipFilename)}\""); - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - } - - /// - /// Saves the skipped update version. - /// - public void SkipUpdate() - { - var version = (string)_releaseObject["tag_name"] ?? ""; - _config.skippedUpdateVersion = version; - Program.MainController.SaveSkippedUpdateVerion(version); - logger.Info($"The update {version} has been skipped and will be ignored next time."); - CloseVersionUpdatePromptWindow(); - } - - /// - /// Closes the update prompt window. - /// - public void CloseVersionUpdatePromptWindow() - { - if (versionUpdatePromptWindow != null) - { - versionUpdatePromptWindow.Close(); - versionUpdatePromptWindow = null; - } - } - } -} diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs deleted file mode 100644 index 1025b073d..000000000 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ /dev/null @@ -1,733 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Windows.Forms; -using NLog; -using Shadowsocks.Controller.Service; -using Shadowsocks.Controller.Strategy; -using Shadowsocks.Model; -using Shadowsocks.Util; -using WPFLocalizeExtension.Engine; - -namespace Shadowsocks.Controller -{ - public class ShadowsocksController - { - private readonly Logger logger; - private readonly HttpClient httpClient; - - // controller: - // handle user actions - // manipulates UI - // interacts with low level logic - #region Members definition - private Thread _trafficThread; - - private Listener _listener; - private PACDaemon _pacDaemon; - private PACServer _pacServer; - private Configuration _config; - private StrategyManager _strategyManager; - private PrivoxyRunner privoxyRunner; - private readonly ConcurrentDictionary _pluginsByServer; - - private long _inboundCounter = 0; - private long _outboundCounter = 0; - public long InboundCounter => Interlocked.Read(ref _inboundCounter); - public long OutboundCounter => Interlocked.Read(ref _outboundCounter); - public Queue trafficPerSecondQueue; - - private bool stopped = false; - - public class PathEventArgs : EventArgs - { - public string Path; - } - - public class UpdatedEventArgs : EventArgs - { - public string OldVersion; - public string NewVersion; - } - - public class TrafficPerSecond - { - public long inboundCounter; - public long outboundCounter; - public long inboundIncreasement; - public long outboundIncreasement; - } - - public event EventHandler ConfigChanged; - public event EventHandler EnableStatusChanged; - public event EventHandler EnableGlobalChanged; - public event EventHandler ShareOverLANStatusChanged; - public event EventHandler VerboseLoggingStatusChanged; - public event EventHandler ShowPluginOutputChanged; - public event EventHandler TrafficChanged; - - // when user clicked Edit PAC, and PAC file has already created - public event EventHandler PACFileReadyToOpen; - public event EventHandler UserRuleFileReadyToOpen; - - public event EventHandler UpdatePACFromGeositeCompleted; - - public event ErrorEventHandler UpdatePACFromGeositeError; - - public event ErrorEventHandler Errored; - - // Invoked when controller.Start(); - public event EventHandler ProgramUpdated; - #endregion - - public ShadowsocksController() - { - logger = LogManager.GetCurrentClassLogger(); - httpClient = new HttpClient(); - _config = Configuration.Load(); - Configuration.Process(ref _config); - _strategyManager = new StrategyManager(this); - _pluginsByServer = new ConcurrentDictionary(); - StartTrafficStatistics(61); - - ProgramUpdated += (o, e) => - { - // version update precedures - if (e.OldVersion == "4.3.0.0" || e.OldVersion == "4.3.1.0") - _config.geositeDirectGroups.Add("private"); - - logger.Info($"Updated from {e.OldVersion} to {e.NewVersion}"); - }; - } - - #region Basic - - public void Start(bool systemWakeUp = false) - { - if (_config.firstRunOnNewVersion && !systemWakeUp) - { - ProgramUpdated.Invoke(this, new UpdatedEventArgs() - { - OldVersion = _config.version, - NewVersion = UpdateChecker.Version, - }); - // delete pac.txt when regeneratePacOnUpdate is true - if (_config.regeneratePacOnUpdate) - try - { - File.Delete(PACDaemon.PAC_FILE); - logger.Info("Deleted pac.txt from previous version."); - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - // finish up first run of new version - _config.firstRunOnNewVersion = false; - _config.version = UpdateChecker.Version; - Configuration.Save(_config); - } - Reload(); - if (!systemWakeUp) - HotkeyReg.RegAllHotkeys(); - } - - public void Stop() - { - if (stopped) - { - return; - } - stopped = true; - if (_listener != null) - { - _listener.Stop(); - } - StopPlugins(); - if (privoxyRunner != null) - { - privoxyRunner.Stop(); - } - if (_config.enabled) - { - SystemProxy.Update(_config, true, null); - } - Encryption.RNG.Close(); - } - - protected void Reload() - { - Encryption.RNG.Reload(); - // some logic in configuration updated the config when saving, we need to read it again - _config = Configuration.Load(); - Configuration.Process(ref _config); - - NLogConfig.LoadConfiguration(); - - logger.Info($"WPF Localization Extension|Current culture: {LocalizeDictionary.CurrentCulture}"); - - // set User-Agent for httpClient - try - { - if (!string.IsNullOrWhiteSpace(_config.userAgentString)) - httpClient.DefaultRequestHeaders.Add("User-Agent", _config.userAgentString); - } - catch - { - // reset userAgent to default and reapply - Configuration.ResetUserAgent(_config); - httpClient.DefaultRequestHeaders.Add("User-Agent", _config.userAgentString); - } - - privoxyRunner = privoxyRunner ?? new PrivoxyRunner(); - - _pacDaemon = _pacDaemon ?? new PACDaemon(_config); - _pacDaemon.PACFileChanged += PacDaemon_PACFileChanged; - _pacDaemon.UserRuleFileChanged += PacDaemon_UserRuleFileChanged; - _pacServer = _pacServer ?? new PACServer(_pacDaemon); - _pacServer.UpdatePACURL(_config); // So PACServer works when system proxy disabled. - - GeositeUpdater.ResetEvent(); - GeositeUpdater.UpdateCompleted += PacServer_PACUpdateCompleted; - GeositeUpdater.Error += PacServer_PACUpdateError; - - _listener?.Stop(); - StopPlugins(); - - // don't put PrivoxyRunner.Start() before pacServer.Stop() - // or bind will fail when switching bind address from 0.0.0.0 to 127.0.0.1 - // though UseShellExecute is set to true now - // http://stackoverflow.com/questions/10235093/socket-doesnt-close-after-application-exits-if-a-launched-process-is-open - privoxyRunner.Stop(); - try - { - var strategy = GetCurrentStrategy(); - strategy?.ReloadServers(); - - StartPlugin(); - privoxyRunner.Start(_config); - - TCPRelay tcpRelay = new TCPRelay(this, _config); - tcpRelay.OnInbound += UpdateInboundCounter; - tcpRelay.OnOutbound += UpdateOutboundCounter; - tcpRelay.OnFailed += (o, e) => GetCurrentStrategy()?.SetFailure(e.server); - - UDPRelay udpRelay = new UDPRelay(this); - List services = new List - { - tcpRelay, - udpRelay, - _pacServer, - new PortForwarder(privoxyRunner.RunningPort) - }; - _listener = new Listener(services); - _listener.Start(_config); - } - catch (Exception e) - { - // translate Microsoft language into human language - // i.e. An attempt was made to access a socket in a way forbidden by its access permissions => Port already in use - if (e is SocketException se) - { - if (se.SocketErrorCode == SocketError.AddressAlreadyInUse) - { - e = new Exception(I18N.GetString("Port {0} already in use", _config.localPort), e); - } - else if (se.SocketErrorCode == SocketError.AccessDenied) - { - e = new Exception(I18N.GetString("Port {0} is reserved by system", _config.localPort), e); - } - } - logger.LogUsefulException(e); - ReportError(e); - } - - ConfigChanged?.Invoke(this, new EventArgs()); - UpdateSystemProxy(); - } - - protected void SaveConfig(Configuration newConfig) - { - Configuration.Save(newConfig); - Reload(); - } - - protected void ReportError(Exception e) - { - Errored?.Invoke(this, new ErrorEventArgs(e)); - } - - public HttpClient GetHttpClient() => httpClient; - public Server GetCurrentServer() => _config.GetCurrentServer(); - public Configuration GetCurrentConfiguration() => _config; - - public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint) - { - IStrategy strategy = GetCurrentStrategy(); - if (strategy != null) - { - return strategy.GetAServer(type, localIPEndPoint, destEndPoint); - } - if (_config.index < 0) - { - _config.index = 0; - } - return GetCurrentServer(); - } - - public void SaveServers(List servers, int localPort, bool portableMode) - { - _config.configs = servers; - _config.localPort = localPort; - _config.portableMode = portableMode; - Configuration.Save(_config); - } - - public void SelectServerIndex(int index) - { - _config.index = index; - _config.strategy = null; - SaveConfig(_config); - } - - public void ToggleShareOverLAN(bool enabled) - { - _config.shareOverLan = enabled; - SaveConfig(_config); - - ShareOverLANStatusChanged?.Invoke(this, new EventArgs()); - } - - #endregion - - #region OS Proxy - - public void ToggleEnable(bool enabled) - { - _config.enabled = enabled; - SaveConfig(_config); - - EnableStatusChanged?.Invoke(this, new EventArgs()); - } - - public void ToggleGlobal(bool global) - { - _config.global = global; - SaveConfig(_config); - - EnableGlobalChanged?.Invoke(this, new EventArgs()); - } - - public void SaveProxy(ForwardProxyConfig proxyConfig) - { - _config.proxy = proxyConfig; - SaveConfig(_config); - } - - private void UpdateSystemProxy() - { - SystemProxy.Update(_config, false, _pacServer); - } - - #endregion - - #region PAC - - private void PacDaemon_PACFileChanged(object sender, EventArgs e) - { - UpdateSystemProxy(); - } - - private void PacServer_PACUpdateCompleted(object sender, GeositeResultEventArgs e) - { - UpdatePACFromGeositeCompleted?.Invoke(this, e); - } - - private void PacServer_PACUpdateError(object sender, ErrorEventArgs e) - { - UpdatePACFromGeositeError?.Invoke(this, e); - } - - private static readonly IEnumerable IgnoredLineBegins = new[] { '!', '[' }; - private void PacDaemon_UserRuleFileChanged(object sender, EventArgs e) - { - GeositeUpdater.MergeAndWritePACFile(_config.geositeDirectGroups, _config.geositeProxiedGroups, _config.geositePreferDirect); - UpdateSystemProxy(); - } - - public void CopyPacUrl() - { - Clipboard.SetDataObject(_pacServer.PacUrl); - } - - public void SavePACUrl(string pacUrl) - { - _config.pacUrl = pacUrl; - SaveConfig(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void UseOnlinePAC(bool useOnlinePac) - { - _config.useOnlinePac = useOnlinePac; - SaveConfig(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void TouchPACFile() - { - string pacFilename = _pacDaemon.TouchPACFile(); - - PACFileReadyToOpen?.Invoke(this, new PathEventArgs() { Path = pacFilename }); - } - - public void TouchUserRuleFile() - { - string userRuleFilename = _pacDaemon.TouchUserRuleFile(); - - UserRuleFileReadyToOpen?.Invoke(this, new PathEventArgs() { Path = userRuleFilename }); - } - - public void ToggleSecureLocalPac(bool enabled) - { - _config.secureLocalPac = enabled; - SaveConfig(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void ToggleRegeneratePacOnUpdate(bool enabled) - { - _config.regeneratePacOnUpdate = enabled; - SaveConfig(_config); - ConfigChanged?.Invoke(this, new EventArgs()); - } - - #endregion - - #region SIP002 - - public bool AskAddServerBySSURL(string ssURL) - { - var dr = MessageBox.Show(I18N.GetString("Import from URL: {0} ?", ssURL), I18N.GetString("Shadowsocks"), MessageBoxButtons.YesNo); - if (dr == DialogResult.Yes) - { - if (AddServerBySSURL(ssURL)) - { - MessageBox.Show(I18N.GetString("Successfully imported from {0}", ssURL)); - return true; - } - else - { - MessageBox.Show(I18N.GetString("Failed to import. Please check if the link is valid.")); - } - } - return false; - } - - public bool AddServerBySSURL(string ssURL) - { - try - { - if (string.IsNullOrWhiteSpace(ssURL)) - return false; - - var servers = Server.GetServers(ssURL); - if (servers == null || servers.Count == 0) - return false; - - foreach (var server in servers) - { - _config.configs.Add(server); - if (server.warnLegacyUrl) - MessageBox.Show(I18N.GetString("Warning: importing {0} from a legacy ss:// link. Support for legacy ss:// links will be dropped in version 5. Make sure to update your ss:// links.", server.ToString())); - } - _config.index = _config.configs.Count - 1; - SaveConfig(_config); - return true; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - } - - public string GetServerURLForCurrentServer() - { - return GetCurrentServer().GetURL(_config.generateLegacyUrl); - } - - #endregion - - #region Misc - - public void ToggleVerboseLogging(bool enabled) - { - _config.isVerboseLogging = enabled; - SaveConfig(_config); - NLogConfig.LoadConfiguration(); // reload nlog - - VerboseLoggingStatusChanged?.Invoke(this, new EventArgs()); - } - - public void ToggleCheckingUpdate(bool enabled) - { - _config.autoCheckUpdate = enabled; - Configuration.Save(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void ToggleCheckingPreRelease(bool enabled) - { - _config.checkPreRelease = enabled; - Configuration.Save(_config); - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void SaveSkippedUpdateVerion(string version) - { - _config.skippedUpdateVersion = version; - Configuration.Save(_config); - } - - public void SaveLogViewerConfig(LogViewerConfig newConfig) - { - _config.logViewer = newConfig; - newConfig.SaveSize(); - Configuration.Save(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - public void SaveHotkeyConfig(HotkeyConfig newConfig) - { - _config.hotkey = newConfig; - SaveConfig(_config); - - ConfigChanged?.Invoke(this, new EventArgs()); - } - - #endregion - - #region Strategy - - public void SelectStrategy(string strategyID) - { - _config.index = -1; - _config.strategy = strategyID; - SaveConfig(_config); - } - - public IList GetStrategies() - { - return _strategyManager.GetStrategies(); - } - - public IStrategy GetCurrentStrategy() - { - foreach (var strategy in _strategyManager.GetStrategies()) - { - if (strategy.ID == _config.strategy) - { - return strategy; - } - } - return null; - } - - public void UpdateInboundCounter(object sender, SSTransmitEventArgs args) - { - GetCurrentStrategy()?.UpdateLastRead(args.server); - Interlocked.Add(ref _inboundCounter, args.length); - } - - public void UpdateOutboundCounter(object sender, SSTransmitEventArgs args) - { - GetCurrentStrategy()?.UpdateLastWrite(args.server); - Interlocked.Add(ref _outboundCounter, args.length); - } - - #endregion - - #region SIP003 - - private void StartPlugin() - { - var server = _config.GetCurrentServer(); - GetPluginLocalEndPointIfConfigured(server); - } - - private void StopPlugins() - { - foreach (var serverAndPlugin in _pluginsByServer) - { - serverAndPlugin.Value?.Dispose(); - } - _pluginsByServer.Clear(); - } - - public EndPoint GetPluginLocalEndPointIfConfigured(Server server) - { - var plugin = _pluginsByServer.GetOrAdd( - server, - x => Sip003Plugin.CreateIfConfigured(x, _config.showPluginOutput)); - - if (plugin == null) - { - return null; - } - - try - { - if (plugin.StartIfNeeded()) - { - logger.Info( - $"Started SIP003 plugin for {server.Identifier()} on {plugin.LocalEndPoint} - PID: {plugin.ProcessId}"); - } - } - catch (Exception ex) - { - logger.Error("Failed to start SIP003 plugin: " + ex.Message); - throw; - } - - return plugin.LocalEndPoint; - } - - public void ToggleShowPluginOutput(bool enabled) - { - _config.showPluginOutput = enabled; - SaveConfig(_config); - - ShowPluginOutputChanged?.Invoke(this, new EventArgs()); - } - - #endregion - - #region Traffic Statistics - - private void StartTrafficStatistics(int queueMaxSize) - { - trafficPerSecondQueue = new Queue(); - for (int i = 0; i < queueMaxSize; i++) - { - trafficPerSecondQueue.Enqueue(new TrafficPerSecond()); - } - _trafficThread = new Thread(new ThreadStart(() => TrafficStatistics(queueMaxSize))) - { - IsBackground = true - }; - _trafficThread.Start(); - } - - private void TrafficStatistics(int queueMaxSize) - { - TrafficPerSecond previous, current; - while (true) - { - previous = trafficPerSecondQueue.Last(); - current = new TrafficPerSecond - { - inboundCounter = InboundCounter, - outboundCounter = OutboundCounter - }; - current.inboundIncreasement = current.inboundCounter - previous.inboundCounter; - current.outboundIncreasement = current.outboundCounter - previous.outboundCounter; - - trafficPerSecondQueue.Enqueue(current); - if (trafficPerSecondQueue.Count > queueMaxSize) - trafficPerSecondQueue.Dequeue(); - - TrafficChanged?.Invoke(this, new EventArgs()); - - Thread.Sleep(1000); - } - } - - #endregion - - #region SIP008 - - - public async Task UpdateOnlineConfigInternal(string url) - { - var onlineServer = await OnlineConfigResolver.GetOnline(url); - _config.configs = Configuration.SortByOnlineConfig( - _config.configs - .Where(c => c.group != url) - .Concat(onlineServer) - ); - logger.Info($"updated {onlineServer.Count} server from {url}"); - return onlineServer.Count; - } - - public async Task UpdateOnlineConfig(string url) - { - var selected = GetCurrentServer(); - try - { - int count = await UpdateOnlineConfigInternal(url); - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - _config.index = _config.configs.IndexOf(selected); - SaveConfig(_config); - return true; - } - - public async Task> UpdateAllOnlineConfig() - { - var selected = GetCurrentServer(); - var failedUrls = new List(); - foreach (var url in _config.onlineConfigSource) - { - try - { - await UpdateOnlineConfigInternal(url); - } - catch (Exception e) - { - logger.LogUsefulException(e); - failedUrls.Add(url); - } - } - - _config.index = _config.configs.IndexOf(selected); - SaveConfig(_config); - return failedUrls; - } - - public void SaveOnlineConfigSource(List sources) - { - _config.onlineConfigSource = sources; - SaveConfig(_config); - } - - public void RemoveOnlineConfig(string url) - { - _config.onlineConfigSource.RemoveAll(v => v == url); - _config.configs = Configuration.SortByOnlineConfig( - _config.configs.Where(c => c.group != url) - ); - SaveConfig(_config); - } - - #endregion - } -} diff --git a/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs b/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs deleted file mode 100644 index d19ea24af..000000000 --- a/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs +++ /dev/null @@ -1,189 +0,0 @@ -using NLog; -using Shadowsocks.Model; -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - -namespace Shadowsocks.Controller.Strategy -{ - class HighAvailabilityStrategy : IStrategy - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - protected ServerStatus _currentServer; - protected Dictionary _serverStatus; - ShadowsocksController _controller; - Random _random; - - public class ServerStatus - { - // time interval between SYN and SYN+ACK - public TimeSpan latency; - public DateTime lastTimeDetectLatency; - - // last time anything received - public DateTime lastRead; - - // last time anything sent - public DateTime lastWrite; - - // connection refused or closed before anything received - public DateTime lastFailure; - - public Server server; - - public double score; - } - - public HighAvailabilityStrategy(ShadowsocksController controller) - { - _controller = controller; - _random = new Random(); - _serverStatus = new Dictionary(); - } - - public string Name - { - get { return I18N.GetString("High Availability"); } - } - - public string ID - { - get { return "com.shadowsocks.strategy.ha"; } - } - - public void ReloadServers() - { - // make a copy to avoid locking - var newServerStatus = new Dictionary(_serverStatus); - - foreach (var server in _controller.GetCurrentConfiguration().configs) - { - if (!newServerStatus.ContainsKey(server)) - { - var status = new ServerStatus(); - status.server = server; - status.lastFailure = DateTime.MinValue; - status.lastRead = DateTime.Now; - status.lastWrite = DateTime.Now; - status.latency = new TimeSpan(0, 0, 0, 0, 10); - status.lastTimeDetectLatency = DateTime.Now; - newServerStatus[server] = status; - } - else - { - // update settings for existing server - newServerStatus[server].server = server; - } - } - _serverStatus = newServerStatus; - - ChooseNewServer(); - } - - public Server GetAServer(IStrategyCallerType type, System.Net.IPEndPoint localIPEndPoint, EndPoint destEndPoint) - { - if (type == IStrategyCallerType.TCP) - { - ChooseNewServer(); - } - if (_currentServer == null) - { - return null; - } - return _currentServer.server; - } - - /** - * once failed, try after 5 min - * and (last write - last read) < 5s - * and (now - last read) < 5s // means not stuck - * and latency < 200ms, try after 30s - */ - public void ChooseNewServer() - { - ServerStatus oldServer = _currentServer; - List servers = new List(_serverStatus.Values); - DateTime now = DateTime.Now; - foreach (var status in servers) - { - // all of failure, latency, (lastread - lastwrite) normalized to 1000, then - // 100 * failure - 2 * latency - 0.5 * (lastread - lastwrite) - status.score = - 100 * 1000 * Math.Min(5 * 60, (now - status.lastFailure).TotalSeconds) - -2 * 5 * (Math.Min(2000, status.latency.TotalMilliseconds) / (1 + (now - status.lastTimeDetectLatency).TotalSeconds / 30 / 10) + - -0.5 * 200 * Math.Min(5, (status.lastRead - status.lastWrite).TotalSeconds)); - logger.Debug(String.Format("server: {0} latency:{1} score: {2}", status.server.ToString(), status.latency, status.score)); - } - ServerStatus max = null; - foreach (var status in servers) - { - if (max == null) - { - max = status; - } - else - { - if (status.score >= max.score) - { - max = status; - } - } - } - if (max != null) - { - if (_currentServer == null || max.score - _currentServer.score > 200) - { - _currentServer = max; - logger.Info($"HA switching to server: {_currentServer.server.ToString()}"); - } - } - } - - public void UpdateLatency(Model.Server server, TimeSpan latency) - { - logger.Debug($"latency: {server.ToString()} {latency}"); - - ServerStatus status; - if (_serverStatus.TryGetValue(server, out status)) - { - status.latency = latency; - status.lastTimeDetectLatency = DateTime.Now; - } - } - - public void UpdateLastRead(Model.Server server) - { - logger.Debug($"last read: {server.ToString()}"); - - ServerStatus status; - if (_serverStatus.TryGetValue(server, out status)) - { - status.lastRead = DateTime.Now; - } - } - - public void UpdateLastWrite(Model.Server server) - { - logger.Debug($"last write: {server.ToString()}"); - - ServerStatus status; - if (_serverStatus.TryGetValue(server, out status)) - { - status.lastWrite = DateTime.Now; - } - } - - public void SetFailure(Model.Server server) - { - logger.Debug($"failure: {server.ToString()}"); - - ServerStatus status; - if (_serverStatus.TryGetValue(server, out status)) - { - status.lastFailure = DateTime.Now; - } - } - } -} diff --git a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs deleted file mode 100644 index be8869da7..000000000 --- a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Shadowsocks.Controller; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Shadowsocks.Controller.Strategy -{ - class StrategyManager - { - List _strategies; - public StrategyManager(ShadowsocksController controller) - { - _strategies = new List(); - _strategies.Add(new BalancingStrategy(controller)); - _strategies.Add(new HighAvailabilityStrategy(controller)); - // TODO: load DLL plugins - } - public IList GetStrategies() - { - return _strategies; - } - } -} diff --git a/shadowsocks-csharp/Controller/System/AutoStartup.cs b/shadowsocks-csharp/Controller/System/AutoStartup.cs deleted file mode 100644 index d7e92580e..000000000 --- a/shadowsocks-csharp/Controller/System/AutoStartup.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using Microsoft.Win32; -using NLog; -using Shadowsocks.Util; - -namespace Shadowsocks.Controller -{ - static class AutoStartup - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - // Don't use Application.ExecutablePath - // see https://stackoverflow.com/questions/12945805/odd-c-sharp-path-issue - - private static string Key = "Shadowsocks_" + Program.ExecutablePath.GetHashCode(); - - public static bool Set(bool enabled) - { - RegistryKey runKey = null; - try - { - runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); - if (runKey == null) - { - logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); - return false; - } - if (enabled) - { - runKey.SetValue(Key, Program.ExecutablePath); - } - else - { - runKey.DeleteValue(Key); - } - // When autostartup setting change, change RegisterForRestart state to avoid start 2 times - RegisterForRestart(!enabled); - return true; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - finally - { - if (runKey != null) - { - try - { - runKey.Close(); - runKey.Dispose(); - } - catch (Exception e) - { logger.LogUsefulException(e); } - } - } - } - - public static bool Check() - { - RegistryKey runKey = null; - try - { - runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); - if (runKey == null) - { - logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); - return false; - } - var check = false; - foreach (var valueName in runKey.GetValueNames()) - { - if (valueName.Equals(Key, StringComparison.InvariantCultureIgnoreCase)) - { - check = true; - continue; - } - - // Remove other startup keys with the same executable path. fixes #3011 and also assures compatibility with older versions - if (Program.ExecutablePath.Equals(runKey.GetValue(valueName).ToString(), StringComparison.InvariantCultureIgnoreCase)) - { - runKey.DeleteValue(valueName); - runKey.SetValue(Key, Program.ExecutablePath); - check = true; - } - } - return check; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - finally - { - if (runKey != null) - { - try - { - runKey.Close(); - runKey.Dispose(); - } - catch (Exception e) - { logger.LogUsefulException(e); } - } - } - } - - [DllImport("kernel32.dll", SetLastError = true)] - static extern int RegisterApplicationRestart([MarshalAs(UnmanagedType.LPWStr)] string commandLineArgs, int Flags); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern int UnregisterApplicationRestart(); - - [Flags] - enum ApplicationRestartFlags - { - RESTART_ALWAYS = 0, - RESTART_NO_CRASH = 1, - RESTART_NO_HANG = 2, - RESTART_NO_PATCH = 4, - RESTART_NO_REBOOT = 8, - } - - // register restart after system reboot/update - public static void RegisterForRestart(bool register) - { - // requested register and not autostartup - if (register && !Check()) - { - // escape command line parameter - string[] args = new List(Program.Args) - .Select(p => p.Replace("\"", "\\\"")) // escape " to \" - .Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " - .ToArray(); - string cmdline = string.Join(" ", args); - // first parameter is process command line parameter - // needn't include the name of the executable in the command line - RegisterApplicationRestart(cmdline, (int)(ApplicationRestartFlags.RESTART_NO_CRASH | ApplicationRestartFlags.RESTART_NO_HANG)); - logger.Debug("Register restart after system reboot, command line:" + cmdline); - } - // requested unregister, which has no side effect - else if (!register) - { - UnregisterApplicationRestart(); - logger.Debug("Unregister restart after system reboot"); - } - } - } -} diff --git a/shadowsocks-csharp/Controller/System/Hotkeys/HotkeyCallbacks.cs b/shadowsocks-csharp/Controller/System/Hotkeys/HotkeyCallbacks.cs deleted file mode 100644 index 08f15836d..000000000 --- a/shadowsocks-csharp/Controller/System/Hotkeys/HotkeyCallbacks.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Reflection; - -namespace Shadowsocks.Controller.Hotkeys -{ - public class HotkeyCallbacks - { - - public static void InitInstance(ShadowsocksController controller) - { - if (Instance != null) - { - return; - } - - Instance = new HotkeyCallbacks(controller); - } - - /// - /// Create hotkey callback handler delegate based on callback name - /// - /// - /// - public static Delegate GetCallback(string methodname) - { - if (string.IsNullOrEmpty(methodname)) throw new ArgumentException(nameof(methodname)); - MethodInfo dynMethod = typeof(HotkeyCallbacks).GetMethod(methodname, - BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); - return dynMethod == null ? null : Delegate.CreateDelegate(typeof(HotKeys.HotKeyCallBackHandler), Instance, dynMethod); - } - - #region Singleton - - private static HotkeyCallbacks Instance { get; set; } - - private readonly ShadowsocksController _controller; - - private HotkeyCallbacks(ShadowsocksController controller) - { - _controller = controller; - } - - #endregion - - #region Callbacks - - private void SwitchSystemProxyCallback() - { - bool enabled = _controller.GetCurrentConfiguration().enabled; - _controller.ToggleEnable(!enabled); - } - - private void SwitchSystemProxyModeCallback() - { - var config = _controller.GetCurrentConfiguration(); - if (config.enabled) - _controller.ToggleGlobal(!config.global); - } - - private void SwitchAllowLanCallback() - { - var status = _controller.GetCurrentConfiguration().shareOverLan; - _controller.ToggleShareOverLAN(!status); - } - - private void ShowLogsCallback() - { - Program.MenuController.ShowLogForm_HotKey(); - } - - private void ServerMoveUpCallback() - { - int currIndex; - int serverCount; - GetCurrServerInfo(out currIndex, out serverCount); - if (currIndex - 1 < 0) - { - // revert to last server - currIndex = serverCount - 1; - } - else - { - currIndex -= 1; - } - _controller.SelectServerIndex(currIndex); - } - - private void ServerMoveDownCallback() - { - int currIndex; - int serverCount; - GetCurrServerInfo(out currIndex, out serverCount); - if (currIndex + 1 == serverCount) - { - // revert to first server - currIndex = 0; - } - else - { - currIndex += 1; - } - _controller.SelectServerIndex(currIndex); - } - - private void GetCurrServerInfo(out int currIndex, out int serverCount) - { - var currConfig = _controller.GetCurrentConfiguration(); - currIndex = currConfig.index; - serverCount = currConfig.configs.Count; - } - - #endregion - } -} diff --git a/shadowsocks-csharp/Controller/System/Hotkeys/Hotkeys.cs b/shadowsocks-csharp/Controller/System/Hotkeys/Hotkeys.cs deleted file mode 100644 index a55591ef4..000000000 --- a/shadowsocks-csharp/Controller/System/Hotkeys/Hotkeys.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Windows.Input; -using GlobalHotKey; - -namespace Shadowsocks.Controller.Hotkeys -{ - public static class HotKeys - { - private static HotKeyManager _hotKeyManager; - - public delegate void HotKeyCallBackHandler(); - // map key and corresponding handler function - private static Dictionary _keymap = new Dictionary(); - - public static void Init(ShadowsocksController controller) - { - _hotKeyManager = new HotKeyManager(); - _hotKeyManager.KeyPressed += HotKeyManagerPressed; - - HotkeyCallbacks.InitInstance(controller); - } - - public static void Destroy() - { - _hotKeyManager.KeyPressed -= HotKeyManagerPressed; - _hotKeyManager.Dispose(); - } - - private static void HotKeyManagerPressed(object sender, KeyPressedEventArgs e) - { - var hotkey = e.HotKey; - HotKeyCallBackHandler callback; - if (_keymap.TryGetValue(hotkey, out callback)) - callback(); - } - - public static bool RegHotkey(HotKey hotkey, HotKeyCallBackHandler callback) - { - UnregExistingHotkey(callback); - return Register(hotkey, callback); - } - - public static bool UnregExistingHotkey(HotKeys.HotKeyCallBackHandler cb) - { - HotKey existingHotKey; - if (IsCallbackExists(cb, out existingHotKey)) - { - // unregister existing one - Unregister(existingHotKey); - return true; - } - else - { - return false; - } - } - - public static bool IsHotkeyExists( HotKey hotKey ) - { - if (hotKey == null) throw new ArgumentNullException(nameof(hotKey)); - return _keymap.Any( v => v.Key.Equals( hotKey ) ); - } - - public static bool IsCallbackExists( HotKeyCallBackHandler cb, out HotKey hotkey) - { - if (cb == null) throw new ArgumentNullException(nameof(cb)); - if (_keymap.Any(v => v.Value == cb)) - { - hotkey = _keymap.First(v => v.Value == cb).Key; - return true; - } - else - { - hotkey = null; - return false; - } - } - - #region Converters - - public static string HotKey2Str( HotKey key ) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - return HotKey2Str( key.Key, key.Modifiers ); - } - - public static string HotKey2Str( Key key, ModifierKeys modifier ) - { - if (!Enum.IsDefined(typeof(Key), key)) - throw new InvalidEnumArgumentException(nameof(key), (int) key, typeof(Key)); - try - { - ModifierKeysConverter mkc = new ModifierKeysConverter(); - var keyStr = Enum.GetName(typeof(Key), key); - var modifierStr = mkc.ConvertToInvariantString(modifier); - - return $"{modifierStr}+{keyStr}"; - } - catch (NotSupportedException) - { - // converter exception - return null; - } - } - - public static HotKey Str2HotKey(string s) - { - try - { - if (string.IsNullOrEmpty(s)) return null; - int offset = s.LastIndexOf("+", StringComparison.OrdinalIgnoreCase); - if (offset <= 0) return null; - string modifierStr = s.Substring(0, offset).Trim(); - string keyStr = s.Substring(offset + 1).Trim(); - - KeyConverter kc = new KeyConverter(); - ModifierKeysConverter mkc = new ModifierKeysConverter(); - Key key = (Key) kc.ConvertFrom(keyStr.ToUpper()); - ModifierKeys modifier = (ModifierKeys) mkc.ConvertFrom(modifierStr.ToUpper()); - - return new HotKey(key, modifier); - } - catch (NotSupportedException) - { - // converter exception - return null; - } - catch (NullReferenceException) - { - return null; - } - } - - #endregion - - private static bool Register(HotKey key, HotKeyCallBackHandler callBack) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (callBack == null) - throw new ArgumentNullException(nameof(callBack)); - try - { - _hotKeyManager.Register(key); - _keymap[key] = callBack; - return true; - } - catch (ArgumentException) - { - // already called this method with the specific hotkey - // return success silently - return true; - } - catch (Win32Exception) - { - // this hotkey already registered by other programs - // notify user to change key - return false; - } - } - - private static void Unregister(HotKey key) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - _hotKeyManager.Unregister(key); - if(_keymap.ContainsKey(key)) - _keymap.Remove(key); - } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Controller/System/ProtocolHandler.cs b/shadowsocks-csharp/Controller/System/ProtocolHandler.cs deleted file mode 100644 index c04d18556..000000000 --- a/shadowsocks-csharp/Controller/System/ProtocolHandler.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.Win32; -using NLog; -using Shadowsocks.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shadowsocks.Controller -{ - static class ProtocolHandler - { - const string ssURLRegKey = @"SOFTWARE\Classes\ss"; - - private static Logger logger = LogManager.GetCurrentClassLogger(); - - public static bool Set(bool enabled) - { - RegistryKey ssURLAssociation = null; - - try - { - ssURLAssociation = Registry.CurrentUser.CreateSubKey(ssURLRegKey, RegistryKeyPermissionCheck.ReadWriteSubTree); - if (ssURLAssociation == null) - { - logger.Error(@"Failed to create HKCU\SOFTWARE\Classes\ss to register ss:// association."); - return false; - } - if (enabled) - { - ssURLAssociation.SetValue("", "URL:Shadowsocks"); - ssURLAssociation.SetValue("URL Protocol", ""); - var shellOpen = ssURLAssociation.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"); - shellOpen.SetValue("", $"{Program.ExecutablePath} --open-url %1"); - logger.Info(@"Successfully added ss:// association."); - } - else - { - Registry.CurrentUser.DeleteSubKeyTree(ssURLRegKey); - logger.Info(@"Successfully removed ss:// association."); - } - return true; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - finally - { - if (ssURLAssociation != null) - { - try - { - ssURLAssociation.Close(); - ssURLAssociation.Dispose(); - } - catch (Exception e) - { logger.LogUsefulException(e); } - } - } - } - - public static bool Check() - { - RegistryKey ssURLAssociation = null; - try - { - ssURLAssociation = Registry.CurrentUser.OpenSubKey(ssURLRegKey, true); - if (ssURLAssociation == null) - { - //logger.Info(@"ss:// links not associated."); - return false; - } - - var shellOpen = ssURLAssociation.OpenSubKey("shell").OpenSubKey("open").OpenSubKey("command"); - return (string)shellOpen.GetValue("") == $"{Program.ExecutablePath} --open-url %1"; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return false; - } - finally - { - if (ssURLAssociation != null) - { - try - { - ssURLAssociation.Close(); - ssURLAssociation.Dispose(); - } - catch (Exception e) - { logger.LogUsefulException(e); } - } - } - } - - } -} diff --git a/shadowsocks-csharp/Controller/System/SystemProxy.cs b/shadowsocks-csharp/Controller/System/SystemProxy.cs deleted file mode 100644 index ee767e128..000000000 --- a/shadowsocks-csharp/Controller/System/SystemProxy.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Windows.Forms; -using NLog; -using Shadowsocks.Model; -using Shadowsocks.Util.SystemProxy; - -namespace Shadowsocks.Controller -{ - public static class SystemProxy - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private static string GetTimestamp(DateTime value) - { - return value.ToString("yyyyMMddHHmmssfff"); - } - - public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) - { - bool global = config.global; - bool enabled = config.enabled; - - if (forceDisable) - { - enabled = false; - } - - try - { - if (enabled) - { - if (global) - { - Sysproxy.SetIEProxy(true, true, "localhost:" + config.localPort.ToString(), null); - } - else - { - string pacUrl; - if (config.useOnlinePac && !string.IsNullOrEmpty(config.pacUrl)) - { - pacUrl = config.pacUrl; - } - else - { - - pacUrl = pacSrv.PacUrl; - } - Sysproxy.SetIEProxy(true, false, null, pacUrl); - } - } - else - { - Sysproxy.SetIEProxy(false, false, null, null); - } - } - catch (ProxyException ex) - { - logger.LogUsefulException(ex); - if (ex.Type != ProxyExceptionType.Unspecific && !noRetry) - { - var ret = MessageBox.Show(I18N.GetString("Error occured when process proxy setting, do you want reset current setting and retry?"), I18N.GetString("Shadowsocks"), MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (ret == DialogResult.Yes) - { - Sysproxy.ResetIEProxy(); - Update(config, forceDisable, pacSrv, true); - } - } - else - { - MessageBox.Show(I18N.GetString("Unrecoverable proxy setting error occured, see log for detail"), I18N.GetString("Shadowsocks"), MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Data/NLog.config b/shadowsocks-csharp/Data/NLog.config deleted file mode 100644 index 05509cfff..000000000 --- a/shadowsocks-csharp/Data/NLog.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/shadowsocks-csharp/Data/abp.js b/shadowsocks-csharp/Data/abp.js deleted file mode 100644 index 7dd0246cc..000000000 --- a/shadowsocks-csharp/Data/abp.js +++ /dev/null @@ -1,826 +0,0 @@ -/* eslint-disable */ -// Was generated by gfwlist2pac in precise mode -// https://github.com/clowwindy/gfwlist2pac - -// 2019-10-06: More 'javascript' way to interaction with main program -// 2019-02-08: Updated to support shadowsocks-windows user rules. - -var proxy = __PROXY__; -var userrules = []; -var rules = []; - -// convert to abp grammar -var re = /^(@@)?\|\|.*?[^\^]$/; -for (var i = 0; i < __RULES__.length; i++) { - var s = __RULES__[i]; - if (s.match(re)) s += "^"; - rules.push(s); -} - -for (var i = 0; i < __USERRULES__.length; i++) { - var s = __USERRULES__[i]; - if (s.match(re)) s += "^"; - userrules.push(s); -} - -/* -* This file is part of Adblock Plus , -* Copyright (C) 2006-2014 Eyeo GmbH -* -* Adblock Plus is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License version 3 as -* published by the Free Software Foundation. -* -* Adblock Plus is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with Adblock Plus. If not, see . -*/ - -function createDict() -{ - var result = {}; - result.__proto__ = null; - return result; -} - -function getOwnPropertyDescriptor(obj, key) -{ - if (obj.hasOwnProperty(key)) - { - return obj[key]; - } - return null; -} - -function extend(subclass, superclass, definition) -{ - if (Object.__proto__) - { - definition.__proto__ = superclass.prototype; - subclass.prototype = definition; - } - else - { - var tmpclass = function(){}, ret; - tmpclass.prototype = superclass.prototype; - subclass.prototype = new tmpclass(); - subclass.prototype.constructor = superclass; - for (var i in definition) - { - if (definition.hasOwnProperty(i)) - { - subclass.prototype[i] = definition[i]; - } - } - } -} - -function Filter(text) -{ - this.text = text; - this.subscriptions = []; -} -Filter.prototype = { - text: null, - subscriptions: null, - toString: function() - { - return this.text; - } -}; -Filter.knownFilters = createDict(); -Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/; -Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/; -Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/; -Filter.fromText = function(text) -{ - if (text in Filter.knownFilters) - { - return Filter.knownFilters[text]; - } - var ret; - if (text.charAt(0) == "!") - { - ret = new CommentFilter(text); - } - else - { - ret = RegExpFilter.fromText(text); - } - Filter.knownFilters[ret.text] = ret; - return ret; -}; - -function InvalidFilter(text, reason) -{ - Filter.call(this, text); - this.reason = reason; -} -extend(InvalidFilter, Filter, { - reason: null -}); - -function CommentFilter(text) -{ - Filter.call(this, text); -} -extend(CommentFilter, Filter, { -}); - -function ActiveFilter(text, domains) -{ - Filter.call(this, text); - this.domainSource = domains; -} -extend(ActiveFilter, Filter, { - domainSource: null, - domainSeparator: null, - ignoreTrailingDot: true, - domainSourceIsUpperCase: false, - getDomains: function() - { - var prop = getOwnPropertyDescriptor(this, "domains"); - if (prop) - { - return prop; - } - var domains = null; - if (this.domainSource) - { - var source = this.domainSource; - if (!this.domainSourceIsUpperCase) - { - source = source.toUpperCase(); - } - var list = source.split(this.domainSeparator); - if (list.length == 1 && (list[0]).charAt(0) != "~") - { - domains = createDict(); - domains[""] = false; - if (this.ignoreTrailingDot) - { - list[0] = list[0].replace(/\.+$/, ""); - } - domains[list[0]] = true; - } - else - { - var hasIncludes = false; - for (var i = 0; i < list.length; i++) - { - var domain = list[i]; - if (this.ignoreTrailingDot) - { - domain = domain.replace(/\.+$/, ""); - } - if (domain == "") - { - continue; - } - var include; - if (domain.charAt(0) == "~") - { - include = false; - domain = domain.substr(1); - } - else - { - include = true; - hasIncludes = true; - } - if (!domains) - { - domains = createDict(); - } - domains[domain] = include; - } - domains[""] = !hasIncludes; - } - this.domainSource = null; - } - return this.domains; - }, - sitekeys: null, - isActiveOnDomain: function(docDomain, sitekey) - { - if (this.getSitekeys() && (!sitekey || this.getSitekeys().indexOf(sitekey.toUpperCase()) < 0)) - { - return false; - } - if (!this.getDomains()) - { - return true; - } - if (!docDomain) - { - return this.getDomains()[""]; - } - if (this.ignoreTrailingDot) - { - docDomain = docDomain.replace(/\.+$/, ""); - } - docDomain = docDomain.toUpperCase(); - while (true) - { - if (docDomain in this.getDomains()) - { - return this.domains[docDomain]; - } - var nextDot = docDomain.indexOf("."); - if (nextDot < 0) - { - break; - } - docDomain = docDomain.substr(nextDot + 1); - } - return this.domains[""]; - }, - isActiveOnlyOnDomain: function(docDomain) - { - if (!docDomain || !this.getDomains() || this.getDomains()[""]) - { - return false; - } - if (this.ignoreTrailingDot) - { - docDomain = docDomain.replace(/\.+$/, ""); - } - docDomain = docDomain.toUpperCase(); - for (var domain in this.getDomains()) - { - if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) - { - return false; - } - } - return true; - } -}); - -function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) -{ - ActiveFilter.call(this, text, domains, sitekeys); - if (contentType != null) - { - this.contentType = contentType; - } - if (matchCase) - { - this.matchCase = matchCase; - } - if (thirdParty != null) - { - this.thirdParty = thirdParty; - } - if (sitekeys != null) - { - this.sitekeySource = sitekeys; - } - if (regexpSource.length >= 2 && regexpSource.charAt(0) == "/" && regexpSource.charAt(regexpSource.length - 1) == "/") - { - var regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i"); - this.regexp = regexp; - } - else - { - this.regexpSource = regexpSource; - } -} -extend(RegExpFilter, ActiveFilter, { - domainSourceIsUpperCase: true, - length: 1, - domainSeparator: "|", - regexpSource: null, - getRegexp: function() - { - var prop = getOwnPropertyDescriptor(this, "regexp"); - if (prop) - { - return prop; - } - var source = this.regexpSource.replace(/\*+/g, "*").replace(/\^\|$/, "^").replace(/\W/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)").replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?").replace(/^\\\|/, "^").replace(/\\\|$/, "$").replace(/^(\.\*)/, "").replace(/(\.\*)$/, ""); - var regexp = new RegExp(source, this.matchCase ? "" : "i"); - this.regexp = regexp; - return regexp; - }, - contentType: 2147483647, - matchCase: false, - thirdParty: null, - sitekeySource: null, - getSitekeys: function() - { - var prop = getOwnPropertyDescriptor(this, "sitekeys"); - if (prop) - { - return prop; - } - var sitekeys = null; - if (this.sitekeySource) - { - sitekeys = this.sitekeySource.split("|"); - this.sitekeySource = null; - } - this.sitekeys = sitekeys; - return this.sitekeys; - }, - matches: function(location, contentType, docDomain, thirdParty, sitekey) - { - if (this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, sitekey)) - { - return true; - } - return false; - } -}); -RegExpFilter.prototype["0"] = "#this"; -RegExpFilter.fromText = function(text) -{ - var blocking = true; - var origText = text; - if (text.indexOf("@@") == 0) - { - blocking = false; - text = text.substr(2); - } - var contentType = null; - var matchCase = null; - var domains = null; - var sitekeys = null; - var thirdParty = null; - var collapse = null; - var options; - var match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null; - if (match) - { - options = match[1].toUpperCase().split(","); - text = match.input.substr(0, match.index); - for (var _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) - { - var option = options[_loopIndex6]; - var value = null; - var separatorIndex = option.indexOf("="); - if (separatorIndex >= 0) - { - value = option.substr(separatorIndex + 1); - option = option.substr(0, separatorIndex); - } - option = option.replace(/-/, "_"); - if (option in RegExpFilter.typeMap) - { - if (contentType == null) - { - contentType = 0; - } - contentType |= RegExpFilter.typeMap[option]; - } - else if (option.charAt(0) == "~" && option.substr(1) in RegExpFilter.typeMap) - { - if (contentType == null) - { - contentType = RegExpFilter.prototype.contentType; - } - contentType &= ~RegExpFilter.typeMap[option.substr(1)]; - } - else if (option == "MATCH_CASE") - { - matchCase = true; - } - else if (option == "~MATCH_CASE") - { - matchCase = false; - } - else if (option == "DOMAIN" && typeof value != "undefined") - { - domains = value; - } - else if (option == "THIRD_PARTY") - { - thirdParty = true; - } - else if (option == "~THIRD_PARTY") - { - thirdParty = false; - } - else if (option == "COLLAPSE") - { - collapse = true; - } - else if (option == "~COLLAPSE") - { - collapse = false; - } - else if (option == "SITEKEY" && typeof value != "undefined") - { - sitekeys = value; - } - else - { - return new InvalidFilter(origText, "Unknown option " + option.toLowerCase()); - } - } - } - if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) - { - if (contentType == null) - { - contentType = RegExpFilter.prototype.contentType; - } - contentType &= ~RegExpFilter.typeMap.DOCUMENT; - } - try - { - if (blocking) - { - return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys, collapse); - } - else - { - return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys); - } - } - catch (e) - { - return new InvalidFilter(origText, e); - } -}; -RegExpFilter.typeMap = { - OTHER: 1, - SCRIPT: 2, - IMAGE: 4, - STYLESHEET: 8, - OBJECT: 16, - SUBDOCUMENT: 32, - DOCUMENT: 64, - XBL: 1, - PING: 1, - XMLHTTPREQUEST: 2048, - OBJECT_SUBREQUEST: 4096, - DTD: 1, - MEDIA: 16384, - FONT: 32768, - BACKGROUND: 4, - POPUP: 268435456, - ELEMHIDE: 1073741824 -}; -RegExpFilter.prototype.contentType &= ~ (RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP); - -function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys, collapse) -{ - RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); - this.collapse = collapse; -} -extend(BlockingFilter, RegExpFilter, { - collapse: null -}); - -function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) -{ - RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); -} -extend(WhitelistFilter, RegExpFilter, { -}); - -function Matcher() -{ - this.clear(); -} -Matcher.prototype = { - filterByKeyword: null, - keywordByFilter: null, - clear: function() - { - this.filterByKeyword = createDict(); - this.keywordByFilter = createDict(); - }, - add: function(filter) - { - if (filter.text in this.keywordByFilter) - { - return; - } - var keyword = this.findKeyword(filter); - var oldEntry = this.filterByKeyword[keyword]; - if (typeof oldEntry == "undefined") - { - this.filterByKeyword[keyword] = filter; - } - else if (oldEntry.length == 1) - { - this.filterByKeyword[keyword] = [oldEntry, filter]; - } - else - { - oldEntry.push(filter); - } - this.keywordByFilter[filter.text] = keyword; - }, - remove: function(filter) - { - if (!(filter.text in this.keywordByFilter)) - { - return; - } - var keyword = this.keywordByFilter[filter.text]; - var list = this.filterByKeyword[keyword]; - if (list.length <= 1) - { - delete this.filterByKeyword[keyword]; - } - else - { - var index = list.indexOf(filter); - if (index >= 0) - { - list.splice(index, 1); - if (list.length == 1) - { - this.filterByKeyword[keyword] = list[0]; - } - } - } - delete this.keywordByFilter[filter.text]; - }, - findKeyword: function(filter) - { - var result = ""; - var text = filter.text; - if (Filter.regexpRegExp.test(text)) - { - return result; - } - var match = Filter.optionsRegExp.exec(text); - if (match) - { - text = match.input.substr(0, match.index); - } - if (text.substr(0, 2) == "@@") - { - text = text.substr(2); - } - var candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g); - if (!candidates) - { - return result; - } - var hash = this.filterByKeyword; - var resultCount = 16777215; - var resultLength = 0; - for (var i = 0, l = candidates.length; i < l; i++) - { - var candidate = candidates[i].substr(1); - var count = candidate in hash ? hash[candidate].length : 0; - if (count < resultCount || count == resultCount && candidate.length > resultLength) - { - result = candidate; - resultCount = count; - resultLength = candidate.length; - } - } - return result; - }, - hasFilter: function(filter) - { - return filter.text in this.keywordByFilter; - }, - getKeywordForFilter: function(filter) - { - if (filter.text in this.keywordByFilter) - { - return this.keywordByFilter[filter.text]; - } - else - { - return null; - } - }, - _checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty, sitekey) - { - var list = this.filterByKeyword[keyword]; - for (var i = 0; i < list.length; i++) - { - var filter = list[i]; - if (filter == "#this") - { - filter = list; - } - if (filter.matches(location, contentType, docDomain, thirdParty, sitekey)) - { - return filter; - } - } - return null; - }, - matchesAny: function(location, contentType, docDomain, thirdParty, sitekey) - { - var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); - if (candidates === null) - { - candidates = []; - } - candidates.push(""); - for (var i = 0, l = candidates.length; i < l; i++) - { - var substr = candidates[i]; - if (substr in this.filterByKeyword) - { - var result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); - if (result) - { - return result; - } - } - } - return null; - } -}; - -function CombinedMatcher() -{ - this.blacklist = new Matcher(); - this.whitelist = new Matcher(); - this.resultCache = createDict(); -} -CombinedMatcher.maxCacheEntries = 1000; -CombinedMatcher.prototype = { - blacklist: null, - whitelist: null, - resultCache: null, - cacheEntries: 0, - clear: function() - { - this.blacklist.clear(); - this.whitelist.clear(); - this.resultCache = createDict(); - this.cacheEntries = 0; - }, - add: function(filter) - { - if (filter instanceof WhitelistFilter) - { - this.whitelist.add(filter); - } - else - { - this.blacklist.add(filter); - } - if (this.cacheEntries > 0) - { - this.resultCache = createDict(); - this.cacheEntries = 0; - } - }, - remove: function(filter) - { - if (filter instanceof WhitelistFilter) - { - this.whitelist.remove(filter); - } - else - { - this.blacklist.remove(filter); - } - if (this.cacheEntries > 0) - { - this.resultCache = createDict(); - this.cacheEntries = 0; - } - }, - findKeyword: function(filter) - { - if (filter instanceof WhitelistFilter) - { - return this.whitelist.findKeyword(filter); - } - else - { - return this.blacklist.findKeyword(filter); - } - }, - hasFilter: function(filter) - { - if (filter instanceof WhitelistFilter) - { - return this.whitelist.hasFilter(filter); - } - else - { - return this.blacklist.hasFilter(filter); - } - }, - getKeywordForFilter: function(filter) - { - if (filter instanceof WhitelistFilter) - { - return this.whitelist.getKeywordForFilter(filter); - } - else - { - return this.blacklist.getKeywordForFilter(filter); - } - }, - isSlowFilter: function(filter) - { - var matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist; - if (matcher.hasFilter(filter)) - { - return !matcher.getKeywordForFilter(filter); - } - else - { - return !matcher.findKeyword(filter); - } - }, - matchesAnyInternal: function(location, contentType, docDomain, thirdParty, sitekey) - { - var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); - if (candidates === null) - { - candidates = []; - } - candidates.push(""); - var blacklistHit = null; - for (var i = 0, l = candidates.length; i < l; i++) - { - var substr = candidates[i]; - if (substr in this.whitelist.filterByKeyword) - { - var result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); - if (result) - { - return result; - } - } - if (substr in this.blacklist.filterByKeyword && blacklistHit === null) - { - blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); - } - } - return blacklistHit; - }, - matchesAny: function(location, docDomain) - { - var key = location + " " + docDomain + " "; - if (key in this.resultCache) - { - return this.resultCache[key]; - } - var result = this.matchesAnyInternal(location, 0, docDomain, null, null); - if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) - { - this.resultCache = createDict(); - this.cacheEntries = 0; - } - this.resultCache[key] = result; - this.cacheEntries++; - return result; - } -}; - -var userrulesMatcher = new CombinedMatcher(); -var defaultMatcher = new CombinedMatcher(); - -var direct = 'DIRECT;'; - -for (var i = 0; i < userrules.length; i++) { - userrulesMatcher.add(Filter.fromText(userrules[i])); -} - -for (var i = 0; i < rules.length; i++) { - defaultMatcher.add(Filter.fromText(rules[i])); -} - -// PAC has no v6 support, it sucks -var ip4Re = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ - -var privateNet = [ - ["10.0.0.0", "255.0.0.0"], - ["127.0.0.0", "255.0.0.0"], - ["172.16.0.0", "255.240.0.0"], - ["192.168.0.0", "255.255.0.0"], -] - -function FindProxyForURL(url, host) { - if (host.match(ip4Re)) { - for (var i = 0; i < privateNet.length; i++) { - if (isInNet(host, privateNet[i][0], privateNet[i][1])) return direct; - } - } - if (userrulesMatcher.matchesAny(url, host) instanceof BlockingFilter) { - return proxy; - } - if (userrulesMatcher.matchesAny(url, host) instanceof WhitelistFilter) { - return direct; - } - // Hack for Geosite, it provides a whitelist... - if (defaultMatcher.matchesAny(url, host) instanceof WhitelistFilter) { - return direct; - } - if (defaultMatcher.matchesAny(url, host) instanceof BlockingFilter) { - return proxy; - } - return direct; -} diff --git a/shadowsocks-csharp/Data/dlc.dat b/shadowsocks-csharp/Data/dlc.dat deleted file mode 100644 index 6ff4f969f..000000000 --- a/shadowsocks-csharp/Data/dlc.dat +++ /dev/null @@ -1,10782 +0,0 @@ - - -VIU -viu.tvapi.viu.now.com^ewcdn[0-9]{2}\.nowe\.com$ viu.com dfp6rglgjqszk.cloudfront.net!d1k2us671qcoau.cloudfront.net!d2anahhhmp1ffz.cloudfront.net -< - BITSQUARE bisq.io bisq.network bitsquare.io - -HUOBI -hbabit.com -hbfile.net  huobi.com huobi.me  huobi.pro huobi.sc huobiasia.viphuobigroup.comhuobitoken.com - - STREPSILS -cepacol.ca cepacol.com dobendan.degraneodin.com.mx -repsils.no strepsils.at strepsils.ch strepsils.cn -cnstrepsils.co.krstrepsils.co.nzstrepsils.co.zastrepsils.com.arstrepsils.com.brstrepsils.com.cn -cnstrepsils.com.costrepsils.com.austrepsils.com.hkstrepsils.com.phstrepsils.com.tw strepsils.es strepsils.fi strepsils.fr strepsils.hr strepsils.hu strepsils.ie strepsils.net strepsils.pl strepsils.pt strepsils.ro strepsils.ru strepsils.si strepsils.usstrepsilsarabia.com - -9NEWS 9cdn.net9nation.com.au 9news.com.au 9now.com.aufreeviewplus.net.au -mi9.com.au -mi9cdn.com nine.com.aunineentertainment.com.aunineentertainmentco.com.auninemediaroom.com.auninemsn.com.austatic9.net.au yourtv.com.au -! - -MASTERCARD mastercard.ae mastercard.at mastercard.az mastercard.ba mastercard.be mastercard.bg mastercard.by mastercard.ca mastercard.ch mastercard.cl mastercard.cn -cnmastercard.co.idmastercard.co.ilmastercard.co.inmastercard.co.jpmastercard.co.kemastercard.co.krmastercard.co.nzmastercard.co.thmastercard.co.ukmastercard.co.vemastercard.co.zamastercard.commastercard.com.armastercard.com.aumastercard.com.bhmastercard.com.brmastercard.com.bzmastercard.com.cn -cnmastercard.com.comastercard.com.cymastercard.com.egmastercard.com.gemastercard.com.gtmastercard.com.hkmastercard.com.kwmastercard.com.lbmastercard.com.mxmastercard.com.mymastercard.com.ngmastercard.com.pemastercard.com.phmastercard.com.samastercard.com.sgmastercard.com.trmastercard.com.twmastercard.com.vemastercard.com.vn mastercard.cz mastercard.de mastercard.dk mastercard.es mastercard.eu mastercard.fi mastercard.fr mastercard.gr mastercard.hr mastercard.hu mastercard.iemastercard.inc mastercard.it mastercard.jo mastercard.ke mastercard.kz mastercard.lu mastercard.md mastercard.my mastercard.nl mastercard.no mastercard.om mastercard.pl mastercard.pt mastercard.qa mastercard.ro mastercard.rs mastercard.ru mastercard.se mastercard.si mastercard.sk mastercard.ua mastercard.usareyoucreditwise.comborderlessprepaid.combranchportal.com idservice.incinclusivegrowthscore.com findacard.com mastercad.commastercard-email.commastercardacademy.commastercardadvisors.commastercardcenter.commastercardcenter.org*&mastercardcenterforinclusivegrowth.orgmastercardconnect.commastercardezsavings.commastercardidtheftalerts.commastercardrestaurant.commastercardservices.commastercardworldwide.commasterpassteststore.commasterintelligence.commcpeaceofmind.commyaccountglobalcash.commycardbenefits.comonedinesfree.com paypass.netpieceofplastic.compredictivetechnologies.com priceless.com pricelessmarketingengine.comsbitravelcard.comshoppercentre.com!smartcommunitiescoalition.com!smartcommunitiescoalition.org startpath.comtaptotokyo.comtasteofpremium.jptravelcontroller.comwwwpaypass.combecomeindex.comcashpassport.cacashpassport.co.zacashpassport.comcashpassport.com.brcashpassport.netcorporatecashpassport.comicashpassport.com.mxintercamcashpassport.com.mx!multicurrencycashpassport.comshinhangmc.commastercardbiz.camastercardbiz.com!mastercardbusinessnetwork.com -nab.com.aunabtravellercard.com.au 53world.com53worldsweeps.comlovethiscity.commastercardmoments.compahabicilemezsurprizler.com priceless.compriceless.com.cn -cn priceless.orgpricelessafrica.compricelessarabia.compricelessaruba.compricelessbeijing.cn -cnpricelessbeijing.com#pricelessbeijing.com.cn -cnpricelesshongkong.compricelesshonolulu.compricelesspick.compricelesssantiago.compricelesssydney.compricelesssurprises.compricelesstoronto.capricelesstv.com simplify.comsimplifycommerce.comcashpassportglobe.comgwktravelex.nl travelex.be travelex.bh travelex.catravelex.co.intravelex.co.jptravelex.co.nztravelex.co.uk travelex.comtravelex.com.autravelex.com.cn -cntravelex.com.hktravelex.com.mytravelex.com.omtravelex.com.tr travelex.de travelex.fr travelex.it travelex.qatravelexae.comtravelexch.comtravelexmoneycard.combidvestbank.co.zaworldcurrencycard.co.zanab.demdex.net&"nationalaustraliaban.tt.omtrdc.net -/ -XVIDEOS xvideos.comxvideos-cdn.com - -ZSH zsh.org -? - BONGACAMS bongacams.com -bcicdn.com bcrncdn.com -E -HCAPTCHA botstop.com hcaptcha.comhcaptchastatus.com -! -MIAOPASIxn--i2ru8q2qg.com -| -MEITUAN dianping.com -maoyan.com meituan.com meituan.net  mtyun.com  neixin.cn sankuai.com - -RT -rt.com -+ -WIKIDOT wdfiles.com wikidot.com -f -OOKLA-SPEEDTEST-ADSOB^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$ -ads -D -RUST rust-lang.org  rustup.rs  crates.io docs.rs - -TLD-CNcn citic baidu -unicomsohu -xn--fiqs8s -xn--fiqz9s xn--1qqw23a xn--xhq521b xn--8y0a063a xn--6frz82g -xn--fiq64b xn--estv75g xn--vuq861b xn--zfr164b -) - ADCOLONY-ADS adcolony.com -ads - - BRILLIANT brilliant.org - -EFF eff.org - -STEEMIT steemit.com - - MANMANBUY manmanbuy.com -3 -PPTV  pplive.cn -pplive.com pptv.com - - -SF-EXPRESS  feng1.comrongechain.comsf-airlines.comsf-cityrush.comsf-express.comsf-financial.com sf-saas.comsf-tech.com.cn sfcar.hk sffix.cn sfgy.org -T - MICROSOFT azure-dns.comazure-dns.info azure-dns.net azure-dns.orgazure-mobile.netazure-sphere.comazure-test.netazure.cn -cn  azure.com  azure.netazurecomcdn.netazurecontainer.ioazurecosmos.netazurecosmosdb.comazurecosmosdb.infoazurecosmosdb.netazuredatabricks.netazuredevopslaunch.comazuredigitaltwin.comazuredigitaltwins.comazuredigitaltwins.netazuredns-prd.infoazuredns-prd.orgazureedge-test.net azureedge.netazureiotcentral.comazureiotsolutions.comazureiotsuite.comazureplanetscale.infoazureplanetscale.netazureserviceprofiler.comazuresmartspaces.netazurestackvalidation.comazurewebsites.netchinacloudapi.cn -cnchinacloudapp.cn -cnchinacloudsites.cn -cn cosmosdb.info cosmosdb.net devopsms.com gotcosmos.commicrosofteca.commicrosoftiotcentral.comserverlesslibrary.nettryfunctions.comwindowsazure.cn -cnwindowsazure.comazure.microsoft.com"azuremarketplace.microsoft.combing bing.com bing.com.cn -cn bing.net bingads.combingagencyawards.combingapistatistics.combingsandbox.combingvisualsearch.com bingworld.combluehatnights.com -dictate.msflipwithsurface.commasalladeloslimites.commicrosoft-give.commicrosoftcloudsummit.commicrosoftdiplomados.commicrosoftlatamholiday.commicrosoftmxfilantropia.com!microsoftpartnersolutions.commsunlimitedcloudsummit.comoffice365love.comoffice365tw.comrenovacionoffice.comsprinklesapp.com cn.bing.com -cn cn.bing.net -cn ditu.live.com -cnbj1.api.bing.com -cnemoi-cncdn.bing.com -cn atom.io  npmjs.com npm.communitydependabot.com -git.io github.blog -github.comgithub.community  github.io githubapp.comgithubassets.comgithubhackathon.comgithubstatus.comgithubusercontent.com/+github-avatars.oss-cn-hongkong.aliyuncs.comFBgithub-production-release-asset-[0-9a-za-z]{6}\.s3\.amazonaws\.comadaptivecards.ioapi-extractor.com -apisof.net appcenter.ms -blazor.netbotframework.comcodethemicrobit.comdevopsassessment.net dot.netgamesstack.comgraphengine.ioinsiderdevtour.com -jwt.ms microbit.orgmicrosoftadc.commicrosoftgamestack.commicrosoftiotinsiderlabs.commicrosoftreactor.cn -cn#microsoftreactor.com.cn -cnmicrosoftreactor.infomicrosoftreactor.netmicrosoftreactor.orgmicrosoftsilverlight.commicrosoftsilverlight.orgmicrosoftsilverlight.netmicrosoftsqlserver.com  mmdnn.commono-project.com msdn.commsinnovationchallenge.commspairlift.commsropendata.com  nuget.org nugettest.orgopentranslatorstothings.orgposhtestgallery.compwabuilder.comreactorms.com.cn -cn -sankie.netsqlserveronlinux.comtimelinestoryteller.comuwpcommunitytoolkit.comvisualstudio-staging.comvisualstudio.comvisualstudio.covisualstudio.co.ukvisualstudio.euvisualstudio.net vfsforgit.com vfsforgit.org vsassets.io xamarin.comdeveloper.microsoft.comdtlgalleryint.cloudapp.net poshtestgallery.cloudapp.net"psg-int-centralus.cloudapp.netpsg-int-eastus.cloudapp.netmicrosoftnews.ccmicrosoftnews.cn -cnmicrosoftnews.commicrosoftnews.netmicrosoftnews.orgmicrosoftnewsforkids.commicrosoftnewsforkids.netmicrosoftnewsforkids.orgmicrosoftnewskids.commicrosoftnewskids.netmicrosoftnewskids.org msn.commsnewskids.commsnewskids.netmsnewskids.org msnkids.com 1drv.com 1drv.mslivefilestore.com onedrive.coonedrive.co.uk onedrive.com onedrive.eu onedrive.net onedrive.orgonedrive.live.comstorage.live.comcallersbane.com minecraft.netminecraftshop.com -mojang.comforzamotorsport.netforzaracingchampionship.com forzarc.comorithegame.comrenovacionxboxlive.comtellmewhygame.com xbox.co xbox.com xbox.eu xbox.org -xbox360.co xbox360.com -xbox360.eu xbox360.org -xboxab.comxboxgamepass.comxboxgamestudios.com xboxlive.cn -cn xboxlive.com -xboxone.co xboxone.com -xboxone.euxboxplayanywhere.comxboxstudios.com  microsoft azure hotmail -office skype windowsxbox microsoft.az microsoft.be microsoft.by microsoft.ca microsoft.cat microsoft.ch microsoft.cl microsoft.com microsoft.cz microsoft.de microsoft.dk microsoft.ee microsoft.es microsoft.eu microsoft.fi microsoft.ge microsoft.hu microsoft.io microsoft.is microsoft.it microsoft.jp microsoft.lt microsoft.lu microsoft.lv microsoft.md microsoft.pl microsoft.pt microsoft.red microsoft.ro microsoft.rs microsoft.ru microsoft.se microsoft.si microsoft.tv microsoft.ua microsoft.uz microsoft.vnfemalefounderscomp.com -m12.vc -aka.msankarazirvesi2018.comapplicationinsights.ioapplicationinsights.net aria.ms asp.net aspnetcdn.com -binads.com bluehatil.combrazilpartneruniversity.com breakdown.mecentralvalidation.com -ch9.mscharticulator.com cloudapp.netcloudappsecurity.comcortanaanalytics.comcortanaskills.comcrmdynint-gcc.com crmdynint.comcrossborderexpansion.com docs.com dynamics.com efproject.netazuredigitaltwin.comfasttrackreadysupport.comfluidpreview.comgameuxmasterguide.com -gears5.com gearspop.comgearstactics.com -gfx.ms -gigjam.com hololens.com -hotmail.co hotmail.com -hotmail.eu hotmail.net hotmail.orghummingbird.ms -ie8.co ie9.com ie10.com ie11.cominternetexplorer.cointernetexplorer.com imaginecup.pl -ingads.comintunewiki.comiotinactionevents.com -kidgrid.tvlatampartneruniversity.com live.com live.com.au live.eu live.netlivingyourambition.com makecode.orgmanagedmeetingrooms.commeetfasttrack.commeetyourdevices.com mepn.commicrosoft-int.commicrosoft-ppe.commicrosoft-sap-events.commicrosoft-sbs-domains.commicrosoft-smb.cn -cnmicrosoft365.commicrosoftaccountguard.commicrosoftads.commicrosoftadvertising.com*µsoftadvertisingregionalawards.commicrosoftaffiliates.commicrosoftcloud.commicrosoftcloudworkshop.com"microsoftcommunitytraining.commicrosoftedge.commicrosofthouse.commicrosofthouse.netmicrosoftinternetsafety.netmicrosoftlatamaitour.commicrosoftlinc.commicrosoftonline-p.commicrosoftonline.com!microsoftpartnercommunity.commicrosoftready.commicrosoftsiteselection.commicrosoftstore.commicrosoftstore.com.hkmicrosoftstream.commicrosoftteams.commicrosofttradein.commicrosofttranslator.commicrosoftuwp.commomentumms.commorphcharts.commpnevolution.comms-studiosmedia.comms365surfaceoffer.com  msads.net msauth.cn -cn -msauth.netmschallenge2018.com -msecnd.net -msedge.net  msft.info msft.net msftauth.cn -cn msftauth.netmsfteducation.ca msftnet.orgmsgamesresearch.com -msocdn.com -msocsp.com msturing.orgmsudalosti.commymicrosoft.comnextechafrica.net nxta.orgo365weve-dev.como365weve-ppe.com o365weve.com -office.com -office.net office365.com onenote.com onenote.net onestore.msopticsforthecloud.netoutingsapp.com outlook.com passport.net -pixapp.netpowerappscdn.netpowerautomate.com powerbi.compowershellgallery.comprojectmurphy.netprojectsangam.com -pxt.ios-microsoft.com  s-msn.com sfbassets.com sfbassets.net -sfx.ms sharepoint.cn -cnsharepoint.comsharepointonline.com  skype.com  skype.netskypeassets.comskypeassets.net staffhub.mssuccesswithteams.com surface.com sway-cdn.com sway.comsysinternals.comtailwindtraders.com -techhub.ms tfsallin.nettrafficmanager.nettranslatetheweb.comunlocklimitlesslearning.com -userpxt.iovideobreakdown.comvideoindexer.aivirtualearth.net vsallin.net -wbd.ms what-fan.netwindows-int.net windows.com windows.net -windows.nl windows8.hkwindowscommunity.netwindowsmarketplace.comwindowsphone-int.comwindowsphone.comwindowssearch.comwindowsupdate.com -winhec.com -winhec.net -yammer.comcdn.office.net -cn(dl.delivery.mp.microsoft.com -cn&download.windowsupdate.com -cn'bg.v4.a.dl.ws.microsoft.com -cn(bg4.v4.a.dl.ws.microsoft.com -cn2&cdn.marketplaceimages.windowsphone.com -cn cn.windowssearch.com -cn#ctldl.windowsupdate.com -cn#developer.microsoft.com -cndocs.microsoft.com -cn"download.microsoft.com -cn/#download.visualstudio.microsoft.com -cnfs.microsoft.com -cn(msgr.dlservice.microsoft.com -cn, msgruser.dlservice.microsoft.com -cn#officecdn.microsoft.com -cn&qh.dlservice.microsoft.com -cnsdx.microsoft.com -cn/#silverlight.dlservice.microsoft.com -cn, slupdate.dlservice.microsoft.com -cn, storeedgefd.dsx.mp.microsoft.com -cn&wl.dlservice.microsoft.com -cn&wscont1.apps.microsoft.com -cn&wscont2.apps.microsoft.com -cnwww.microsoft.com -cn&xb.dlservice.microsoft.com -cn/+img-prod-cms-rt-microsoft-com.akamaized.netimg-s-msn-com.akamaized.netmwf-service.akamaized.net)%officecdn-microsoft-com.akamaized.net3/statics-marketingsites-eus-ms-com.akamaized.net40statics-marketingsites-wcus-ms-com.akamaized.net -n -TWITCH ext-twitch.tv  jtvnw.net  ttvnw.net  twitch.tv twitchcdn.net twitchsvc.net - -MZED mzed.com - - OPENRESTY openresty.org -< -QUIP quip-cdn.com quip.comquipelements.com -6 - RULEOFLAWrolfoundation.orgrolsociety.org -6 -4CHAN 4cdn.org  4chan.org 4channel.org - -CATEGORY-ANTICENSORSHIP ahmia.fi akasha.worldbrowserleaks.combypasscensorship.org citizenlab.cadat.foundationdemocracy.earth eff.orgepicbrowser.com ffprofile.comfreenetproject.org fteproxy.org  gfw.press -gfw.report greatfire.orgguardianproject.infohaveibeenpwned.compwnedpasswords.com -geti2p.net ipfs.io  jitsi.orggetlantern.org -lantern.io  ntc.partyonionshare.org ooni.org openvpn.netprivacytools.io psiphon3.comshadowsocks.org softether.orgsoftether-download.com stunnel.org telex.cctorproject.org tribler.org tunsafe.comtwister.net.co -uproxy.org  v2ray.com -v2ray.cool  v2fly.org vuvuzela.io -whonix.org wikileaks.org wireguard.com -zeronet.io -z -LINE -lin.ee line.me line.naver.jp line-apps.com line-cdn.net line-scdn.net -nhncorp.jp - -PAXFUL -paxful.com - -RFI -rfi.fr - -1PONDO  1pondo.tv -% -36KR 36kr.com 36krcdn.com - -HK01 hk01.com - -PRESS  press.vin -H - ZYNGA-ADS zynga.com -ads#zyngaplayersupport.com -ads -. - -AMAZON-ADS amazon-adsystem.com -ads -% - BAIDU-ADS a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -ads -e -METART -metart.commetartnetwork.com metcams.comnaiadsystems.com  nsimg.net - -IQIYI 71.am  iqiyi.com iqiyipic.com -pps.tv -ppsimg.com qiyi.com qiyipic.com -qy.net -71edge.coma.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -ads -9 -NIKKEI -nikkei.com  nikkei.jp nikkei.co.jp - -OPENAI -openai.com - - CATEGORY-ADS apilog-web.acfun.cn -ads adcolony.com -ads -adjust.com -ads a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -ads amazon-adsystem.com -adsiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -ads applovin.com -ads -applvn.com -ads atom-data.io -adsanalytics-data.io -ads ironbeast.io -ads a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -adsmcs.snssdk.com -adsx.clearbitjs.com -ads bluekai.com -adsfout.jp -ads gmossp-sp.jp -adsi-mobile.co.jp -ads im-apps.net -ads impact-ad.jp -ads ladsp.com -ads microad.co.jp -ads -microad.jp -ads socdm.com -ads emogi.com -adsdata.flurry.com -ads2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -ads!assets.growingio.com -adsmlog.hiido.com -adsylog.hiido.com -adsstatic.hotjar.com -adsclick.hunantv.com -adsda.hunantv.com -ads da.mgtv.com -adslog.hunantv.com -adslog.v2.hunantv.com -adsres.hunantv.com -adsv2.log.hunantv.com -adsinner-active.mobi -adsa.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -adsads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads log-sdk.gifshow.com -adswlog.kuaishou.com -ads leanplum.com -ads#api.game.letvstore.com -ads ark.letv.com -ads dc.letv.com -adsdev.dc.letv.com -ads fz.letv.com -ads g3.letv.com -adsletv.allyes.com -adsminisite.letv.com -adsmsg.m.letv.com -adsn.mark.letv.com -adsplog.dc.letv.com -adspro.hoye.letv.com -ads pro.letv.com -ads stat.letv.com -ads"static.app.m.letv.com -ads decide.mixpanel.com -ads mopub.com -adsevt.mxplay.com -adsanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads"js-agent.newrelic.com -ads ogury.com -ads -presage.io -adsogury.co -ads onesignal.com -adsOB^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$ -ads cdn.pocoiq.cn -ads oct.pocoiq.cn -adsads.pubmatic.com -adscdn.segment.com -ads"static.sensorsdata.cn -ads 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -adsadnet.sohu.com -ads ads.sohu.com -ads!adv-sv-show.focus.cn -ads aty.sohu.com -adsdata.vod.itc.cn -adsepro.sogou.com -ads go.sohu.com -adsgolden1.sogou.com -adsimp.optaim.com -adsinte.sogou.com -adsinte.sogoucdn.com -adslu.sogoucdn.com -ads pv.sohu.com -adstheta.sogoucdn.com -adstrack.sohu.com -adsuranus.sogou.com -adsv2.reachmax.cn -ads)e8aeb8bbdbbd7.cdn.sohucs.com -ads)643108e7617ef.cdn.sohucs.com -adssupersonic.com -adssupersonicads.com -ads -ssacdn.com -ads)supersonicads-a.akamaihd.net -ads g1.tagtic.cn -adsxy-log.tagtic.cn -adsssp.api.tappx.com -ads ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads uberads.com -adsalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads!unityads.unity3d.com -ads#unityads.unitychina.cn -ads ads.wteam.xyz -ads#collector.xhamster.com -adsad.intl.xiaomi.com -ads ad.mi.com -ads ad.xiaomi.com -adsadmob.xiaomi.com -adsad1.xiaomi.com -adsmistat.xiaomi.com -adstracking.miui.com -adsadv.sec.miui.com -ads"adv.sec.intl.miui.com -ads"misc.in.duokanbox.com -ads ad.hpplay.cn -adsadeng.hpplay.cn -ads atianqi.com -ads kuyun.com -ads umeng.com -ads ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -adsactives.youku.com -ads ad.api.3g.youku.com -ads$ad.api.mobile.youku.com -ads ad.mobile.youku.com -ads(a-dxk.play.api.3g.youku.com -ads atm.youku.com -ads#b.smartvideo.youku.com -adsc.yes.youku.com -adsdas.api.youku.com -ads!das.mobile.youku.com -ads!dev-push.m.youku.com -adsdl.g.youku.com -adsdmapp.youku.com -adse.stat.ykimg.com -ads#gamex.mobile.youku.com -ads!guanggaoad.youku.com -ads hudong.pl.youku.com -ads!huodong.pl.youku.com -ads"huodong.vip.youku.com -ads hz.youku.com -adsiyes.youku.com -ads l.ykimg.com -adslstat.youku.com -ads mobilemsg.youku.com -ads msg.youku.com -adsmyes.youku.com -ads p.l.youku.com -ads pl.youku.com -ads#passport-log.youku.com -adsp-log.ykimg.com -adspush.m.youku.com -ads r.l.youku.com -ads s.p.youku.com -adssdk.m.youku.com -adsstat.youku.com -ads$statis.api.3g.youku.com -ads&store.tv.api.3g.youku.com -ads&store.xl.api.3g.youku.com -adstdrec.youku.com -adstest.ott.youku.com -ads!test.sdk.m.youku.com -ads v.l.youku.com -adsval.api.youku.com -ads wan.youku.com -adsykatr.youku.com -adsykrec.youku.com -ads cvnad.com -ads -iyoowi.com -ads zynga.com -ads#zyngaplayersupport.com -ads 51y5.net a.adtng.com -aaxads.com -ad-srv.netad.api.moji.comad.wang502.com adbutter.netads.trafficjunky.net adtechus.comcfts1tifqr.comcontentabc.comimg-bss.csdn.net imglnkc.com imglnkd.comja2.gamersky.com jl3.yjaxa.topkepler-37b.com -lqc006.coms4yxaqyq95.comshhs-ydd8x2.yjrmss.cnstatic.javhd.comtrafficfactory.biz adx.36kr.comclick.ali213.netpbmp.ali213.net^pinggai\d\.caixin\.com$adq.chinaso.comstat.chinaso.com toboads.comdn-growing.qbox.me ad.unimhk.comdas-rpt-log.ucloud.cn adxprtz.com exoclick.com -exosrv.comcdn.advertserve.comcdn.banclip.comcdn.tsyndicate.com - - CHINAUNICOM  10010.com 10010.com.cnchinaunicom.comchinaunicom.com.cnchinaunicom.com.hkchinaunicomglobal.commychinaunicom.com  wo.com.cn - -D100 d100.net - -RTINGS -rtings.com - -FARFETCHfarfetch-apps.comfarfetch-contents.com farfetch.cn -cn farfetch.comfarfetch.com.cn -cn farfetch.netfarfetch.net.cn -cn  fflnk.net - -NICONICOnico -nicodic.jp nicomanga.jp niconico.com nicoseiga.jp nicovideo.jp nimg.jp simg.jp -% -RTHK etvonline.hk rthk.hk -+ -VAPTCHA vaptcha.com vaptcha.net - -ERJI erji.net - -HEROKU  heroku.co -heroku.com  heroku.me herokuapp.com herokucdn.comherokucharge.com herokussl.comheroku-app.comheroku-charge.com codei.sh -codeish.co -codeish.io - -MINISOloveminiso.com.br miniso-au.com miniso-bh.com miniso-np.com miniso-nz.com  miniso.by  miniso.ca miniso.cn -cn  miniso.co miniso.co.id miniso.co.tz -miniso.com miniso.com.es miniso.com.gr miniso.com.mx miniso.com.pl miniso.com.py miniso.com.tr  miniso.ge  miniso.ie  miniso.jp  miniso.ma  miniso.my  miniso.pe  miniso.pk  miniso.ua  miniso.uz minisohome.cn -cnminisoindia.comminisojordan.comminisokorea.comminisolife.co.zaminisothailand.comminisovietam.vn minsio.cn -cn myminiso.com -d - JUSTMYSOCKSjustmysocks.netjustmysocks1.netjustmysocks2.netjustmysocks4.net - -XINGRZ  xingrz.me -& -XNXX xnxx.com xnxx-cdn.com - -BOC -boc.cnbankofchina.combocaviation.com  boccfc.cn  bocgi.com bocgins.com  bochk.com bocichina.com bocigroup.com  bocim.combocservices.com.cn -6 -CATEGORY-COLLABORATE-CN shimo.im  yuque.com - -DINGTALK ddurl.to dingding.xin dingtalk.cn dingtalk.com dingtalk.netdingtalkapps.com laiwang.com lwurl.to -W -MOJANGcallersbane.com minecraft.netminecraftshop.com -mojang.com - -SPOTIFY byspotify.com pscdn.co scdn.co spoti.fispotify-everywhere.com spotify.comspotify.designspotifycdn.comspotifycharts.comspotifycodes.comspotifyforbrands.comspotifyjobs.com - -BEATS100beatscheap.com1monsterbeatsbydreus.com2013beatsbydrdreonline.com2013beatsbydrdreshop.com2013beatsbydreshop.com2013beatshdcybermonday.com2013cheapestbeats.com2013newbeatsworld.com2013pascherbeatsbydre.com2014cheapbeatsbydre.com 2beatsdre.com2buybeatsbydre.com2drdrebeats.com40shopping.com4beatsbydre.com5beatsbydre.comaaagradeheadphones.com -aanaan.comabcheadphones.comabeatsbydrdre.com aeasyshop.com -airmay.com -ajtalk.com albeats.com annstores.netaokwholesale.netaucheapbeats.comaudiobeatsau.comaudiobeatsbydre.comaudiomonsterbeatsonline.comauricularemonsterbeats.com#auricularesbeatsbaratosshop.comauricularesbeatsmarkt.comausbeatsbydrdre.combag-glasses1.com bandgirlz.com baxsound.com bbestmall.com -beatbd.combeatbydre2013.combeatbydremonster.com!beatbydreheadphonesonsale.combeatbydrekopen.combeatbydreuk2014.combeatdrdres.combeatfactoryoutlets.combeatmonstersaustralia.netbeats-by-dre-australia.combeats-bydrdre.netbeats-bydre-mall.combeats-bydrecheapsale.combeats-bydreoutletonline.combeats-bydreoutletsale.combeats-bydreoutletsale.netbeats-bydreoutletssale.netbeats-bydrestore.combeats-bydreuk.combeats-deal.combeats-dre-us.com"beats-headphones-buy-cheap.combeats-headphones.usbeats-sale.combeats-seller.combeats-soaho.com  beats1.cc beats1.cn -cn  beats1.tv beats1.com.cn -cn beats123.com beats2.com.cn -cn beats4.cn -cn -beats4.netbeats4outlets.combeats4salecheap.combeatsallsale.combeatsaudifonos.combeatsaudiobydre.combeatsaudiobydre.netbeatsaudios.netbeatsbeatsmonster.combeatsbestdeals.combeatsblackfridaydeals.netbeatsblackfridayretails.combeatsboxingdayuksale.combeatsbydrdre-headphones.combeatsbydrdre-officials5.combeatsbydrdre-online.combeatsbydrdre-onsale.combeatsbydrdre-store.combeatsbydrdre-store.usbeatsbydrdre4sale.combeatsbydrdrebiz.combeatsbydrdrecustom.combeatsbydrdredanmark.combeatsbydrdremall.combeatsbydrdreoutlet.combeatsbydrdres.combeatsbydrdresale.netbeatsbydrdrestore.combeatsbydrdreus.com!beatsbydre-beatsheadphone.combeatsbydre-chen.combeatsbydre-club.combeatsbydre-headphones.com!beatsbydre-headphonesshop.combeatsbydre-mall.combeatsbydre-outlet.combeatsbydre-outletsale.netbeatsbydre-outletstore.combeatsbydre-sell.combeatsbydre-store.combeatsbydre-studio.combeatsbydre-us.com beatsbydre.jpbeatsbydre2081.combeatsbydre411.combeatsbydre4usales.combeatsbydreauofficial.combeatsbydreausale.net beatsbydreaustralia-sale.com!beatsbydreaustraliaonline.com"beatsbydreaustraliaonlines.com beatsbydreaustraliasales.combeatsbydrebeatsby.com!beatsbydreblackfriday2013.com beatsbydreblackfridaypro.combeatsbydreboxingdayca.combeatsbydrecasquesfr.com#beatsbydrecheap-outletstore.combeatsbydrecheaper.com"beatsbydrecheaponlinesales.combeatsbydrecolors.com beatsbydrecustomwireless.com&"beatsbydrecybermondaydeals2013.com&"beatsbydrecybermondaydeals2013.netbeatsbydredanmarks.com"beatsbydredealsblackfriday.com"beatsbydredealscybermonday.combeatsbydrediscount.com beatsbydrediscountonline.netbeatsbydredr.combeatsbydreexecutivesale.com beatsbydreformall2013-nl.com beatsbydreforsalesonline.com beatsbydreforshop2013-nl.combeatsbydreforstore.combeatsbydreforyououtlet.combeatsbydrefr.com!beatsbydrefrcasquepascher.combeatsbydrefriday.combeatsbydregot.combeatsbydrehd.combeatsbydrehd.netbeatsbydreheadphones-nz.combeatsbydreheadphonesolo.combeatsbydrehut.combeatsbydreinexpensive.combeatsbydreireland-sales.com beatsbydreirelandonlines.combeatsbydreirelandsale.com beatsbydreirelandsonline.combeatsbydremall2013.com"beatsbydremonsteraustralia.combeatsbydrenls.combeatsbydrenorge1.net!beatsbydreofficialdanmark.combeatsbydreoksale.combeatsbydreol.combeatsbydreonlie2013-nl.com!beatsbydreonlines-ireland.combeatsbydreonlines-uk.combeatsbydreonlinesale-nz.combeatsbydreoordopjes.combeatsbydreoslo.combeatsbydreoutletsale.combeatsbydreoutletscheap.combeatsbydrerealstore.combeatsbydres-shop.combeatsbydresale-uk.combeatsbydresalemall2013.combeatsbydresales.us'#beatsbydresalesonline-australia.com beatsbydresaleonlines-nz.combeatsbydresdanmark.netbeatsbydreshop-uk.combeatsbydreshops.netbeatsbydresingapores.combeatsbydresingaporesale.combeatsbydrespeakers.combeatsbydresold.com%!beatsbydresolohdonline-canada.combeatsbydresonline-nz.combeatsbydrestorevip.combeatsbydressale.combeatsbydressolo.combeatsbydresstudio.com"beatsbydrestudio-australia.combeatsbydrestudio.combeatsbydretoutlet.combeatsbydreuk.combeatsbydreuk.netbeatsbydreus.combeatsbydrevipde.combeatsbydrew.combeatsbydrsmonsterinusa.combeatsbymusic.netbeatsbysdrbre.combeatsbysdrdres.combeatscasque-france.combeatscheap-nz.combeatscheap-uk.combeatscheapforsale.combeatscollection2014.combeatscustomblackfriday.combeatsdanmark2013.combeatsdrdre-headphones.combeatsdrdre-it.combeatsdrdre-solo.combeatsdrdre2014.combeatsdrdrecuffie.netbeatsdrdrekaufenschweiz.netbeatsdrdreneon.combeatsdre-monster.com beatsdre.netbeatsdre4cheap.combeatsdrecheap.combeatsdredreheadphones.combeatsdreforsale.combeatsdreinau.combeatsdremonster-uk.com!beatsdrenewcolorful4usale.combeatsdreoutletsale.combeatsdresale2013.combeatsdresalestore.combeatsdresolo2013.combeatsdreus.combeatsearbudsheadphoness.combeatsfacstore.combeatsfactoroutlets.combeatsfactorycollection.combeatsfactoryoutles.combeatsforcheap-usa.combeatsforme.combeatsfranceofficiel.combeatselectronic.netbeatselectronics.com -beatsep.cn -cn beatsep.com beatsep.net beatsheadphones-discount.combeatsheadphones1.combeatsheadphones2u.combeatsheadphonesale.combeatsheadphonesdealer.combeatsheadphonesforcheap.netbeatsheadphonesonline.combeatsheadphoness.combeatsheadphonestudio.combeatsheadphonesusamall.combestshoesale2014.netbeatshopuk.combeatsincanada.combeatsinsingapore.combeatsireland.net beatsjoy.combeatsmonstersales.combeatsmusic.combeatsmusic.wang beatsneon.combeatsnzsale.com beatsodre.combeatsofdre-usa.combeatsonblackfriday2013.com beatsone.netbeatsoutlet.netbeatsoutletanytime.combeatsoutletonlines.combeatspascher-bydre.combeatspascher-fr.netbeatspascher.netbeatspaschers.netbeatspillnewcolor.combeatspills.combeatspromonsterjp.combeatspromotions.combeatssales.combeatssaleus.combeatssaustraliabuy.combeatssbyaustralia.combeatssbydredanmark.combeatsshop-uk.combeatsshop-usa.combeatsshopstore.combeatssingapores.combeatssingaporeshop.com beatsstudiohodetelefoner.combeatsstudiosite.combeatstoreusa.com beatsua.combeatswholesale.usbeatswirelesscheap.combeatswirelesscuffie.combegin-trade.combestbeats4u.com -biitii.comblackfridaydrebeatsnew.comblackfridaydrebeatsshop.combloommicroventures.com bookonsky.net -bossae.combrandeasygo.combrandelectronic.combrandproducts1688.com brands098.combusinesswebwise.combuy-from-shanghai.combuybeatsbydre-uk.combuybeatsheadphonesbydre.combuycheapbeatsbus.combuycheapbeatsbydre.combuycheapbeatsbydreshop.combuycheapbeatsdreuk.combuydrdrebeatbox.combuyminibeatbox.combuyonlineheadphones.combuyshoponly.combuzzardflapper.combybeatsdre.combydrebeats.comcanada-beatsbydre.com casque-fr.com casquebeatsbydrdresolohd.comcasquebeatsdocteurdre.comcasquebeatsdre2013.comcasquebeatsenligne.comcasquebeatsfracheter.comcasquebeatsmer.netcasquebeatsofficiel-fr.comcasquebeatspascher2013.comcasquebeatssolo.netcasquedrdrebeatssfr.comcasquemonsterbeats.com#casquemonsterbeatsbydre2013.comcasquesbeatsaudio.comcheap-beats-by-dre.netcheap-beatsbydre.comcheapbagshoes.com cheapbeats.uscheapbeats365.comcheapbeats4sale.netcheapbeatsaustraliasale.comcheapbeatsbus.comcheapbeatsbydr.comcheapbeatsbydrdrepro.comcheapbeatsbydre-au.comcheapbeatsbydre99.comcheapbeatsbydrefau.comcheapbeatsbydremall.comcheapbeatsbydremonster.comcheapbeatsbydrenz.netcheapbeatsbydreonsale.com cheapbeatsbydreoutlet-nz.comcheapbeatsbydreoutlet.com"cheapbeatsbydreoutlets2013.comcheapbeatsbydresale.comcheapbeatsbydreshop.comcheapbeatsdrdresolo.com$ cheapbeatsbydrestudioedition.com"cheapbeatsbydrestudioutlet.comcheapbeatsdrestudios.comcheapbeatsheadphone2014.comcheapbeatsheadphones.uscheapbeatsie.comcheapbeatsla.comcheapbeatssale4u.comcheapbeatsshopbydre.comcheapcustombeatsbydre.comcheapdrdrebeats8.comcheapdrdrebeatsca.comcheapdrebeats8.netcheaperbeatsbydresale.comcheapestbeatsdrdre.comcheapheadphonesland.comcheapheadphonessale.comcheapheadsetmall.comcheapheadsetshop.com cheapmonsterbeatsbydrdre.com!cheapmonsterbeatsheadsets.com cheapmonstersbeatsonsale.comcheapmonsterbeatssale.comcheapmonsterbeatsusa.uscheapnewbeatsbydre.comcheapsalemonster.comcheapshoesvip.comcheapwireless04.comcheapwirelessbeats.comcheerwholesale.uschihair-straightener.comchinaclothesstore.comciattackers.com -cmhalq.com cnshopin.comcoolmonster.netcozydrdrebeats.com cqcorea.comcuffiesaldi.comcustombeatsbydrebuy.comcustombeatsdeals.comcustombeatsforcheap.comcustombeatsny.comcustombeatssbydreus.comcustomdrdrebeats.comcustomizedbeatbydre.comcustomizedbeatsbydre.comcustomizedbeatsdre.comcybermondaybeats4sale.comdanemarket.comdanmarkbeatsbydrdre.comdctbeatsbydre.comdealsbeatsblackfriday.comdetaliczny.comdiscountbeatsbydre-us.comdiscountbeatsstore.comdkbeatsbydre.comdrbeatsukmart.comdrdre-beats.comdrdrebeats-chen.comdrdrebeats-headphone.comdrdrebeats-usa.comdrdrebeatsale.comdrdrebeatsbillig.comdrdrebeatsdesale.comdrdrebeatsdiscount.comdrdrebeatsforu.comdrdrebeatsretail2013.comdrdrebeatssale7.comdrdrebeatsuk.comdrdreheadphonebeats.comdrdreheadphonekey.comdrdreheadphonesusstore.comdrdremonster-beats.comdrdremonsterdre.comdrdreprobeatssale.com drdrefnac.comdrebeats-australia.comdrebeats-france.comdrebeats-monster.com!drebeats-monsteraustralia.comdrebeats-monsterusa.comdrebeats-singapore.comdrebeats-singapore.netdrebeats-singaporecheap.comdrebeats-singaporecheap.netdrebeats-solo.comdrebeats-studio.comdrebeatsaustralia-cheap.comdrebeatsaustralia-cheap.netdrebeatsbuy.comdrebeatsbydreoutlet.comdrebeatscanada.comdrebeatsdeutschland.netdrebeatsforsaleus.comdrebeatsheadphones-nz.comdrebeatshome.comdrebeatsoutletstore.comdrebeatsoldes.comdrebeatspill.comdrebeatssite.com#drebeatssolocybermondaysale.comdrebeatsstudio2013.comdrebeatstudio.com drebyby.comdremonsterbeatsoutlets.comdreprobeats.com dreprofy.com -dtsell.comearphonescheapest.comebuyheadphones.comesbeatsbydrebuy.cometheadphones.comfamosascalvas.comfindmybeats.comfr-beatsbydrestore.comfrancecasquebeatssolde.comfrcasquesbeats.com free-aa.com free-sns.com frishoes.comgamebeforethegame.comgetdrebeatssale.comgmnetworks.net gobeatsye.comgobuyonlinestore.net goodsdunk.netheadphonepubs.comheadphones-cool.comheadphones-dre.com headphones-outlet-online.comheadphonesbeatsaudio.comheadphonesbeatsbydre.comheadphoneses.comheadphoneshome.comheadphoneshotsales.comheadphonesol.comheadphonesretailer.comheadphonessupply.comheadphonezip.comheadset987.comhellokittybeats.com highbolt.net highdefinitionbeatsbydre.com hipaa6020.com hipzoom.nethonawalaan.comhotbeatsonsale.comhotmonsterbeats.comhottestheadphonesonline.com  hrsaz.com ibeats-uk.comibeatsbydre.ccibeatsbydre.comigoshopping.net illianacomputerrecycling.comimonsterbeats.comincentivetravelgifts.comioffergift.com islamiccenterofnewlondon.comitaliabeatsbydrdre.com jkbeats.comkickshatchannel.comkobbeatssbydredk.comkopfhorergunstigshop.comleecountytimes.com lexuemei.comlovebeatsdr.com lovemarca.comluckyissue.com -lyzsxx.com mairbeats.com -makeeu.commallheadphone.commbeats-tech.commbheadphone.com -micstl.commmonsterheadphones.netmonbeats2013.commonster-beats-by-dr-dre.com monster-beats-headphones.commonsterbeats-cheap.commonsterbeats-headphone.commonsterbeats-onsale.commonsterbeats-solo.commonsterbeats-solo.netmonsterbeats365buy.commonsterbeats8beatsbydre.commonsterbeatsale.commonsterbeatsalestore.commonsterbeatssalg.commonsterbeatsau.commonsterbeatsbydrdre-nz.commonsterbeatsbydrdre-usa.com!monsterbeatsbydrdrecanada.com!monsterbeatsbydrdrestudio.commonsterbeatsbydre-nz.commonsterbeatsbydre-usa.commonsterbeatsbydre2015.com'#monsterbeatsbydreaustraliacheap.com!monsterbeatsbydrebilligde.com monsterbeatsbydrefactory.commonsterbeatsbydrenew.commonsterbeatsbydres.commonsterbeatsbydreshop.commonsterbeatscasques.commonsterbeatscommunity.commonsterbeatsdrdrecheap.commonsterbeatsfactory.netmonsterbeatsfinland.commonsterbeatsforsale.commonsterbeatsheadphone.commonsterbeatsheadphones.netmonsterbeatshere.commonsterbeatsitaly.commonsterbeatsmall.commonsterbeatsnegozi.netmonsterbeatsok.com!monsterbeatsonlinestoreuk.commonsterbeatsoutlet.usmonsterbeatspascher.netmonsterbeatsru.commonsterbeatssales.commonsterbeatsshops.netmonsterbeatsstore.com#monsterbeatssydneyaustralia.commonsterbeatstang.commonsterbeatstienda.commonsterbeatstudio.commonsterbydrebeat.com!monstercasquebeatspascher.netmonstercheapbeatss.commonsterdrebeats-canada.netmonsterdrebeats-usa.commonsterdrebeats-usa.netmonsterheadphone.netmonsterproduct.netmonstersbeatbydres.commonstersdebea.commonstershopcheapbeats.netmoreheadphones.com -ms4dre.commybeatsbydrestudio.commybeatsbydreuk.commybeatscheapbydre.commyferrariheadphones.comnamemybeats.com neborder.comnegozimonsterbeats.comnegoziomonsterbeats.comnewbeatsblackfriday.com newbemany.comnewdrediscount.comnewestbeatsbydre.comnewlysprung.netnewmonst1erbeatsto1re.com!newmonsterbeatsheadphones.comnewschristmasshopping.comnflhotsalejerseys.com nicesshop.netofferairjordanlebron.comofficialbeatsbydreshop.comofficialbeatsbydrestore.comofficialdrdre.comofficialheadphone.com ogameblog.comonbeatsbydresale.com onlinemonsterbeatsonsale.com onsalekey.comonsaletrend.comoutletbeatsshop.compascherbeatsmonster.compaulsimon-music.comperfectkickz.netpersoneelsland.compicknicekicks.netpickshoesclothes.com pillbeatsblackfridaysale.compocketbiketrader.compowerbeats2wireless.compowerbeatsbydrdre.compowerbeatsbydre.compromonsterbeatsbydre.comprostudiobeatscybersale.com  rbsgr.comrepsneakermall.com repswing.com!salebeatslasteststyle4you.comsaleblackfridaydrebeats.comsalecheaphandbags.com samebags.com sanvaras.com -scnshop.cc -shoers.com shoestop2.comshop-headphones.netshopbeatsdre.com shopbydre.comshopcustomizedbeats.comshopdrebeats.comshopmonsterbeats.comshopping-days.netshoppinguheadphones.comsneaker666.comsneakerpage.netsneakerskick.comsolostudioksale.com soso7778.comspecialtyheadphones.comspiritclubs.comstraightouttasomewhere.comstudiobeatsbydrdre.comsulwerphoto.comsunglassessale2014.comsuperearsenjoy.comsupplybestjerseys.comsustainthesound.comsverigebeatsbydrdre.com szcheapmonsterheadphones.comszwinnertechnology.com takegoto.comthe-monster-beats.comthebeatsbydre.comthebeatsbydre.netthebeatsheadphonesale.com -theuab.nettopbeatsbydrdreoutlet.comtopbeatsdealer.comtopbeatsforsale.com tradevip1.com tteshop.com ubnw.net -unstyle.us&"usa-beatsbydreheadphonesonsale.net  v-has.com vip-beats.comvipbeatsbydre.comvipheadphones.com vipshoes2.com  wasdj.comwholesale-exporter1.comwholesalediscountpurses.comwholesalefine.comwholesaleonlinemart.comworldcoinpay.comyourmonsterbeats.com%!beatselectronicsllc.tt.omtrdc.net - - CANONICAL launchpad.netlaunchpadlibrarian.comlaunchpadlibrarian.netlaunchpadlibrarian.org snapcraft.io snapstore.io -ubuntu.com -ubuntu.netubuntuforums.org canonical.com - -GOLANG go-lang.com go-lang.net go-lang.org -go.dev  godoc.org -golang.com -golang.net -golang.org - -REDTUBE redtube.com -F -SOGOU -go2map.com sogo.com  sogou.com sogoucdn.com -. -XDAxda-developers.com xda-cdn.com - -ATYPI  atypi.org - -BEEG beeg.com - - -ONIONSHAREonionshare.org -i -RAZER  razer.comrazersupport.com razerzone.com razerzone.jprespawnbyrazer.com - - SPEEDTEST  cdnst.net cellmaps.com ekahau.cloud -ekahau.com  ookla.comooklaserver.net pingtest.net speedtest.co speedtest.netspeedtestcustom.com webtest.netOB^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$ -ads -* -DUOWAN -duowan.com dwstatic.com - -NETFLIX fast.com -netflix.ca netflix.com netflix.netnetflixinvestor.comnetflixtechblog.com nflxext.com nflximg.com nflximg.netnflxsearch.net -nflxso.net nflxvideo.net -. - POCKETCASTS -pca.stpocketcasts.com - -OONI ooni.org -: - SOFTETHER softether.orgsoftether-download.com - -YUEWEN -yuewen.com hongxiu.com lrts.me qdmm.com -qidian.com readnovel.com tingbook.com webnovel.com -xs8.cn xxsy.net -$ -AFP afp.com afpforum.com - -BESTBUY bby.combbycastatic.ca bbycloud.combbycontent.combbycontent.net bbyintl.com bbystatic.com  bbyurl.us bestbhy.combestbuy-audio.com"bestbuy-communityrelations.combestbuy-giftcard.infobestbuy-int.combestbuy-jobs.com -bestbuy.ca -bestbuy.cn -cn bestbuy.combestbuy.com.cn -cnbestbuy.com.mx bestbuy.infobestbuy24x7solutions.combestbuybusiness.com'#bestbuybusinessadvantageaccount.combestbuycanada.cabestbuycanada.combestbuycanadaltd.cabestbuycharityclassic.combestbuycoffeemakers.combestbuyethics.combestbuyforbusiness.cabestbuygsm.combestbuyideax.com!bestbuyphotoworkshoptours.combestbuyrewards.combestbuyrewardzone.ca bestbuys.combestbuysgeeksquad.combestbuysolutions.netbestbuystores.combestbuytradein.com bestbuyus.com besztbuy.com bextbuy.combienvenuechezbestbuy.ca cowboom.com futureshop.cainitproducts.commagnoliaav.com.cn -cn mybestbuy.commybestbuyclaims.commyrewardzone.com myrz.compacifickitchenandhome.comtiendabestbuy.comunbrandedproducts.comwelcometobestbuy.cawolfatbestbuy.comwolfatbestbuy.netwww-bestbuystores.comgeek-squad-support.comgeek-squad.netgeek-squad.orggeek-squads.net geeksquad.ca geeksquad.cc geeksquad.com geeksquad.net geeksquad.tvgeeksquadcares.comgeeksquadcentral.comgeeksquadforums.comgeeksquadonline.comgeeksquadprotectionplan.orggeeksquadservices.orggeeksquadwebroot.org gscanada.info -gsccdn.com  gslink.usonlinegeeksquad.comrocketfishproducts.com dealtree.orgtechliquidators.com -( -CHINAZ -chinaz.com -chinaz.net - -NTC  ntc.party - - SITEPOINT sitepoint.com - - STACKEXCHANGE askubuntu.comblogoverflow.commathoverflow.netserverfault.com sstatic.net stackapps.com stackauth.comstackexchange.com stackmod.blogstackoverflow.blogstackoverflow.comstackoverflow.emailstackoverflowbusiness.comstacksnippets.net superuser.com tex-talk.netthesffblog.com - -VUVUZELA vuvuzela.io - - YUNFANJIASU yfcache.com -yfcalc.com yfcloud.com  yfdts.net  yfp2p.net -yfscdn.net yunfancdn.com -" - ACCUWEATHERaccuweather.com - -FTEPROXY fteproxy.org -( -IMGUR  imgur.com imgurinc.com -@ -HAVEIBEENPWNEDhaveibeenpwned.compwnedpasswords.com -Q -NEWSMAXandysparis.com newsmax.com -newsmax.in newsmaxtv.com - -TELEX telex.cc - -WEBEX webex.ca webex.co.in webex.co.it webex.co.jp webex.co.kr webex.co.nz webex.co.uk  webex.com webex.com.au webex.com.br webex.com.cn -cn webex.com.hk webex.com.mx webex.de webex.es webex.fr -y -FONTSHOP fontbook.com fontshop.comfstopimages.com40fontshop-prod-responsive-images.s3.amazonaws.com -, -GANJI  ganji.comganjistatic1.com -D - -LIVEJASMINlivejasmin.com dditsadn.com dditscdn.com -( -TAPTAP -tapimg.com -taptap.com -; -WJX -sojump.com -wjx.cn wjx.com wjx.top -. -CHINADIGITALTIMESchinadigitaltimes.net -+ -FLATPAK flathub.org flatpak.org - - KINDGIRLS kindgirls.com -8 - HOMEDEPOThomedepot-static.com homedepot.com -2 -ZOOM zoom.com zoom.com.cn zoom.us - - -CHATURBATEchaturbate.com -. -KUAIKAN kkmh.comkuaikanmanhua.com -m -SUNING cnsuning.com -suning.comsuningcloud.comsuningestate.comsuningholdings.com - - BYTEDANCE -bytecdn.cn bytedance.com bytedance.net bytedns.net byteimg.com -douyin.com huaxiong.comjokecommunity.comneihanshequ.com -pstatp.com -snssdk.com toutiao.comtoutiaocdn.comtoutiaocloud.comtoutiaohao.comtoutiaohao.net toutiaoimg.cntoutiaoimg.comtoutiaoimg.nettoutiaopage.com -wukong.comwukongwenda.cnwukongwenda.com zijiecdn.com zijiecdn.net  zjcdn.com  baike.com -ixigua.com ixgvideo.com  365yg.com - -HACPAI -hacpai.com -3 -QUORA  quora.com quoracdn.net qr.ae -V - JETBRAINS jetbrains.com jetbrains.net datalore.iokotlinlang.org - -MORTEIN -mortein.cn -cn mortein.co.in mortein.co.nz mortein.co.za mortein.commortein.com.aumortein.com.brmortein.com.ngmortein.com.pkmorteincam.compifpafarabia.com - -VILAVPN vilavpn.com vilavpn.xyz vilavpn1.xyz vilavpn2.xyz vilavpn3.xyz vilavpn4.xyz vilavpn5.xyz vilavpn6.xyz vilavpn7.xyz - -WARROOM warroom.org -% - -CHINAPOWERchinapower.csis.org -# - EMOGI-ADS emogi.com -ads -~ -GITLAB -gitlab.com  gitlab.io -gitlab.netgitlab-static.net.*gitlab-assets.oss-cn-hongkong.aliyuncs.com - -WWE wwe.com wwe9.com -$ - APPLEINSIDERappleinsider.com - -QIHOO360 75.team -75team.com baomitu.com -360.cn 360.com -360-jr.com 360chou.com360daikuan.com360huzhubao.com -360jie.com360jinrong.net -360kan.com 360kuai.com  360os.com 360panyun.cn 360safe.com 360shouji.com 360taojin.com360totalsecurity.com 360tpcdn.com360webcache.com anquanke.com  haoso.com -haosou.com haosou.com.cn -haosou.net i360mall.com ludashi.com nicaifu.com  nzwgs.com  qhimg.com qhimgs0.com qhimgs1.com qhimgs2.com qhimgs3.com qhimgs4.com qhimgs5.com qhimgs6.com  qhmsg.com  qhres.com qhstatic.com qhupdate.com -qihuapi.cn qihucdn.com  qihoo.com qikucdn.com -so.com sou.com urlqh.cnxiaoluyouxuan.comxiaoluzhidian.com xjietiao.com -youhua.com  yunpan.cn - -VANISHo2action.co.krpowerofresolve.capowerofresolve.comspraynwash.com  vanish.at  vanish.be  vanish.ch  vanish.cl vanish.cn -cn vanish.co.id vanish.co.il vanish.co.in vanish.co.nz vanish.co.uk vanish.co.za vanish.com.ar vanish.com.br vanish.com.co vanish.com.hr vanish.com.mx vanish.com.my vanish.com.pe vanish.com.sg vanish.com.tr  vanish.de  vanish.dk  vanish.es  vanish.fi  vanish.fr  vanish.hu  vanish.ie  vanish.it  vanish.nl  vanish.no  vanish.pl  vanish.pt  vanish.ro  vanish.ru  vanish.se  vanish.si  vanish.skvanisharabia.comvanishbancaseulook.com.brvanishcentroamerica.com vanishinfo.czvanishstains.comvanishstains.com.au - -NETEASE 126.com 126.link 126.net 127.com 127.net 163.com163industry.com 163mail.com 163qiyukf.com -163yun.com 166.net -haowu.linkicourse163.org  kaola.com -lofter.com  nease.netnetease-inc.com netease.com -netease.im -qiyukf.com ydstatic.com yeah.net -youdao.comanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads -. -OP op.ggopgg-static.akamaized.net - - -SHUZIYIMINshuziyimin.org - - LARACASTS laracasts.com - -QUANTIL quantil.com - -CBScbs showtime cbs.comcbscorporation.com cbsi.com -cbsi.video -cbsiam.com  cbsig.net -cbsimg.netcbsinteractive.comcbsistatic.com cbsivideo.com cbsnews.comcbspressexpress.com cbssports.com cbsstatic.com cbssvideo.com -viacbs.com -viacom.com viacomcbs.comviacomcbspressexpress.com paramount.com -+ -COWLEVEL cowlevel.net  cdncl.net -x -FASTLY -fastly.com -fastly.netfastlylabs.com fastlylb.netfastly-terrarium.com -zencdn.net - -VOLVO volvodynafleetonline.comescandinavia-arg.comindependentoperatorcn.com road-crew.com udfs.com vfsco.at vfsco.be vfsco.bg vfsco.ca vfsco.ch vfsco.cl vfsco.co.uk vfsco.co.za  vfsco.com vfsco.com.au vfsco.com.br vfsco.com.cn -cn vfsco.com.tr vfsco.cz vfsco.de vfsco.dk vfsco.ee vfsco.es vfsco.fi vfsco.fr vfsco.hu vfsco.ie vfsco.in vfsco.it vfsco.jp vfsco.kr vfsco.lt vfsco.lv vfsco.mx vfsco.nl vfsco.no vfsco.pe vfsco.pl vfsco.ro vfsco.ru vfsco.se vfsco.sk vfsco.us vgcareers.netvolvo-truck.nl volvo.cavolvo.cn -cn  volvo.com volvo.com.cn -cn volvo.sevolvobrandshop.com volvobuses.at volvobuses.be volvobuses.ca volvobuses.ch volvobuses.covolvobuses.co.nzvolvobuses.co.ukvolvobuses.co.zavolvobuses.comvolvobuses.com.arvolvobuses.com.auvolvobuses.com.brvolvobuses.com.kwvolvobuses.com.ptvolvobuses.com.tw volvobuses.de volvobuses.dk volvobuses.es volvobuses.fi volvobuses.fr volvobuses.hk volvobuses.hu volvobuses.in volvobuses.it volvobuses.jo volvobuses.kr volvobuses.ma volvobuses.mx volvobuses.my volvobuses.no volvobuses.om volvobuses.ph volvobuses.pk volvobuses.pl volvobuses.ru volvobuses.se volvobuses.sg volvobuses.tn volvobuses.us volvoce.comvolvodefense.com volvogroup.be volvogroup.cn -cnvolvogroup.comvolvogroup.com.brvolvogroup.com.cn -cn volvogroup.de volvogroup.fr volvogroup.jp volvogroup.kr volvogroup.mx volvogroup.nl volvogroup.pe volvogroup.pl volvogroup.ru volvogroup.sevolvogrouptruckcenter.nlvolvomerchandise.comvolvopenta.comvolvopenta.com.br volvopenta.de volvopenta.es volvopenta.fr volvopenta.it volvopenta.nl volvopenta.se volvopenta.usvolvosaatio.fivolvospares.comvolvotrucks.aevolvotrucks.alvolvotrucks.amvolvotrucks.atvolvotrucks.azvolvotrucks.bavolvotrucks.bevolvotrucks.bgvolvotrucks.byvolvotrucks.chvolvotrucks.clvolvotrucks.co.aovolvotrucks.co.bwvolvotrucks.co.ilvolvotrucks.co.mzvolvotrucks.co.navolvotrucks.co.ukvolvotrucks.co.nzvolvotrucks.co.zavolvotrucks.co.zmvolvotrucks.co.zwvolvotrucks.comvolvotrucks.com.arvolvotrucks.com.auvolvotrucks.com.bnvolvotrucks.com.brvolvotrucks.com.cn -cnvolvotrucks.com.covolvotrucks.com.kwvolvotrucks.com.ptvolvotrucks.com.trvolvotrucks.com.twvolvotrucks.com.uavolvotrucks.czvolvotrucks.devolvotrucks.dkvolvotrucks.eevolvotrucks.esvolvotrucks.fivolvotrucks.frvolvotrucks.gevolvotrucks.grvolvotrucks.hkvolvotrucks.hrvolvotrucks.huvolvotrucks.idvolvotrucks.involvotrucks.itvolvotrucks.jpvolvotrucks.kgvolvotrucks.krvolvotrucks.kzvolvotrucks.ltvolvotrucks.lvvolvotrucks.mavolvotrucks.mdvolvotrucks.mkvolvotrucks.mnvolvotrucks.myvolvotrucks.netvolvotrucks.nlvolvotrucks.novolvotrucks.pevolvotrucks.phvolvotrucks.pkvolvotrucks.plvolvotrucks.psvolvotrucks.qavolvotrucks.rovolvotrucks.rsvolvotrucks.ruvolvotrucks.sevolvotrucks.sgvolvotrucks.sivolvotrucks.skvolvotrucks.tmvolvotrucks.tnvolvotrucks.uyvolvotruckcenter.bevolvotruckcenter.dkvolvotruckcenter.esvolvotruckcenter.fivolvotruckcenter.itvolvotruckcenter.krvolvotruckcenter.kzvolvotruckcenter.nlvolvotruckcenter.novolvotruckcenter.sevolvotruckrental.beudtrucksmeena.com - -JITSI  jitsi.org - -REURL reurl.cc - - THEGUARDIAN theguardiandev-guardianapis.comdev-theguardian.com -gu-web.netguardianapis.comguardianapps.co.uk -guim.co.uktheguardian.comtheguardiandns.com -X -FACEBOOKaccountkit.comatscaleconference.com botorch.org -buck.build buckbuild.comcomponentkit.org draftjs.org -f8.comfacebookappcenter.infofacebookappcenter.netfacebookappcenter.orgfacebookconnect.comfacebookdevelopergarage.comfaciometrics.com fasttext.cc fbf8.com fbinfer.com fblitho.com fbredex.com -fbrell.com flow.dev flow.org flowtype.org frescolib.org hacklang.org hhvm.commakeitopen.com mcrouter.net mcrouter.orgmessengerdevelopers.com -ogp.meopengraphprotocol.comopengraphprotocol.org  parse.com pyrobot.org  react.com reactjs.com reactjs.org recoiljs.org rocksdb.com rocksdb.net rocksdb.orgyogalayout.comdevelopers.facebook.com!achat-followers-instagram.com#acheter-followers-instagram.com$ acheterdesfollowersinstagram.com!acheterfollowersinstagram.combookstagram.comcarstagram.comcdninstagram.comchickstagram.com ig.me  igcdn.com igsonar.com igtv.com imstagram.com imtagram.cominstaadder.cominstachecker.cominstafallow.cominstafollower.cominstagainer.com instagda.com instagify.cominstagmania.com instagor.cominstagram-brand.cominstagram-engineering.cominstagram-help.cominstagram-press.cominstagram-press.net instagram.cominstagramci.cominstagramcn.cominstagramdi.cominstagramhashtags.netinstagramhilecim.cominstagramhilesi.orginstagramium.cominstagramizlenme.cominstagramkusu.cominstagramlogin.cominstagramm.cominstagramn.cominstagrampartners.cominstagramphoto.cominstagramq.cominstagramsepeti.cominstagramtakipcisatinal.netinstagramtakiphilesi.cominstagramtips.cominstagramtr.com instagran.cominstagranm.com instagrem.com instagrm.cominstagtram.com instagy.cominstamgram.cominstangram.cominstanttelegram.cominstaplayer.net instastyle.tv instgram.com intagram.com intagrm.com intgram.comkingstagram.comlnstagram-help.comtheinstagramhack.comoninstagram.comonline-instagram.comonlineinstagram.comweb-instagram.netwwwinstagram.comfbmessenger.comm.me messenger.com -nbabot.net binoculus.com buyoculus.com ocul.usoculus-china.com -oculus.comoculus2014.com oculus3d.comoculusblog.comoculusbrand.comoculuscasino.net oculuscdn.comoculusconnect.comoculusdiving.comoculusforbusiness.comoculusrift.com oculusvr.compowersunitedvr.com wa.mewhatsapp-plus.infowhatsapp-plus.mewhatsapp-plus.net whatsapp.cc whatsapp.com whatsapp.info whatsapp.net whatsapp.org whatsapp.tvwhatsappbrand.comaboutfacebook.com accessfacebookfromschool.com acebooik.com acebook.comadvancediddetection.comaskfacebook.netaskfacebook.org -atdmt2.com atlasdmt.comatlasonepoint.combuyingfacebooklikes.comcareersatfb.comcelebgramme.comchina-facebook.com click-url.comcomo-hackearfacebook.comcrowdtangle.com dacebook.comdlfacebook.comdotfacebook.comdotfacebook.netexpresswifi.com faacebok.com faacebook.com faasbook.com facbebook.com facbeok.com -facboo.com facbook.com facbool.com facboox.com faccebook.comfaccebookk.com facdbook.com facdebook.com face-book.com faceabook.com facebboc.com facebbook.comfacebboook.com facebcook.com facebdok.com facebgook.com facebhook.com facebkkk.com facebo-ok.com faceboak.com facebock.com facebocke.com facebof.com faceboik.com facebok.comfacebokbook.com facebokc.com facebokk.com facebokok.com faceboks.com facebol.com facebolk.com facebomok.com faceboo.com facebooa.com faceboob.comfaceboobok.com facebooc.com faceboock.com facebood.com facebooe.com faceboof.com facebooi.com facebooik.com facebooik.org facebooj.comfacebook-corp.comfacebook-covid-19.comfacebook-ebook.comfacebook-forum.comfacebook-inc.comfacebook-login.comfacebook-newsroom.comfacebook-newsroom.orgfacebook-pmdcenter.comfacebook-pmdcenter.netfacebook-pmdcenter.orgfacebook-privacy.comfacebook-program.comfacebook-studio.comfacebook-support.orgfacebook-texas-holdem.comfacebook-texas-holdem.net facebook.br facebook.ca facebook.cc facebook.comfacebook.design facebook.net facebook.hu facebook.in fecebook.net facebook.nl facebook.org facebook.se facebook.shop facebook.tv facebook.us facebook.wangfacebook123.orgfacebook30.comfacebook30.netfacebook30.orgfacebook4business.comfacebookads.com"facebookadvertisingsecrets.comfacebookatschool.comfacebookawards.comfacebookblueprint.netfacebookbrand.comfacebookbrand.net3/facebookcanadianelectionintegrityinitiative.comfacebookcareer.comfacebookcheats.comfacebookck.comfacebookclub.comfacebookcom.comfacebookconsultant.orgfacebookcoronavirus.comfacebookcovers.orgfacebookcredits.infofacebookdating.netfacebookdusexe.orgfacebookemail.comfacebookenespanol.comfacebookexchange.comfacebookexchange.netfacebookfacebook.comfacebookflow.comfacebookgames.comfacebookgraphsearch.comfacebookgraphsearch.infofacebookgroups.comfacebookhome.ccfacebookhome.comfacebookhome.infofacebookhub.com facebooki.comfacebookinc.comfacebookland.comfacebooklikeexchange.comfacebooklive.comfacebooklivestaging.netfacebooklivestaging.orgfacebooklogin.comfacebooklogin.infofacebookloginhelp.netfacebooklogs.comfacebookmail.comfacebookmail.tvfacebookmanager.infofacebookmarketing.info facebookmarketingpartner.com!facebookmarketingpartners.comfacebookmobile.comfacebookmsn.comfacebooknews.comfacebooknfl.comfacebooknude.comfacebookofsex.comfacebookook.comfacebookpaper.comfacebookpay.comfacebookphonenumber.netfacebookphoto.comfacebookphotos.comfacebookpmdcenter.comfacebookpoke.netfacebookpoke.orgfacebookpoker.infofacebookpokerchips.infofacebookporn.netfacebookporn.orgfacebookporno.netfacebookportal.com facebooks.comfacebooksafety.comfacebooksecurity.netfacebookshop.comfacebooksignup.netfacebooksite.netfacebookstories.comfacebookstudios.netfacebookstudios.orgfacebooksupplier.comfacebooksuppliers.comfacebookswagemea.comfacebookswagstore.comfacebooksz.comfacebookthreads.netfacebooktv.netfacebooktv.orgfacebookvacation.com facebookw.comfacebookworld.com facebool.com facebool.info facebooll.com faceboom.com faceboon.com faceboonk.comfaceboooik.com faceboook.com faceboop.com faceboot.com faceboox.com facebopk.com facebpook.com facebuk.com facebuok.com facebvook.com facebyook.com facebzook.comfacecbgook.com facecbook.com facecbook.org facecook.com facecook.org facedbook.com faceebok.com faceebook.com faceebot.com facegbok.com facegbook.com faceobk.com faceobok.com faceobook.com faceook.comfacerbooik.com facerbook.com facesbooc.comfacesounds.com facetook.com facevbook.com facewbook.co facewook.comfacfacebook.com facfebook.com fackebook.com facnbook.com facrbook.com facvebook.com facwebook.com facxebook.com fadebook.com -faebok.com faebook.com faebookc.com faeboook.com faecebok.com faesebook.comfafacebook.com faicbooc.com fasebokk.com fasebook.com faseboox.com favebook.com faycbok.com -fb.careers -fb.com fb.me fbacebook.com fbbmarket.comfbboostyourbusiness.com  fbcdn.com  fbcdn.netfbfeedback.com -fbhome.com fbidb.io  fbinc.comfbinnovation.comfbmarketing.com  fbreg.com -fbrpms.com  fbsbx.com  fbsbx.netfbsupport-covid.netfbthirdpartypixel.comfbthirdpartypixel.netfbthirdpartypixel.org  fburl.comfbworkmail.com fcacebook.com fcaebook.com fcebook.com fcebookk.comfcfacebook.comfdacebook.info feacboo.com feacbook.com feacbooke.com feacebook.com fecbbok.com fecbooc.com fecbook.com feceboock.com feceboox.com fececbook.com  feook.com ferabook.com fescebook.com fesebook.com ffacebook.com fgacebook.com ficeboock.com fmcebook.com fnacebook.com fosebook.com fpacebook.com fqcebook.com fracebook.com  freeb.comfreebasics.comfreebasics.net -freebs.comfreefacebook.comfreefacebook.netfreefacebookads.netfreefblikes.comfreindfeed.comfriendbook.info friendfed.comfriendfeed-api.comfriendfeed-media.comfriendfeed.comfriendfeedmedia.com fsacebok.com fscebook.comfundraisingwithfacebook.comfunnyfacebook.orgfutureofbusinesssurvey.org gacebook.com gameroom.comgfacecbook.com -groups.comhackerfacebook.comhackfacebook.comhackfacebookid.comhifacebook.info!howtohackfacebook-account.comhsfacebook.comhttpfacebook.comhttpsfacebook.comhttpwwwfacebook.com i.org internet.org klik.me liverail.com liverail.tvlogin-account.netmarkzuckerberg.commidentsolutions.commobilefacebook.commoneywithfacebook.com myfbfans.com newsfeed.com nextstop.comonline-deals.netopencreate.orgreachtheworldonfacebook.com rocksdb.orgshopfacebook.comsportsfacebook.comsportstream.comsupportfacebook.comterragraph.comthefacebook.comthefacebook.net thefind.comtoplayerserver.comviewpointsfromfacebook.comwhyfacebook.com worldhack.comwww-facebook.comwwwfacebok.comwwwfacebook.comwwwmfacebook.comzuckerberg.comzuckerberg.netfacebookwork.com -redkix.com workplace.comworkplaceusecases.comfbcdn-a.akamaihd.net -S -PORNHUB -phncdn.com phprcdn.com pornhub.compornhubpremium.com - -NIXOS  nixos.org -E -PIXIV  pixiv.net  pixiv.org pixiv.co.jp  pximg.net - -WEBFLOW webflow.com -* -PAGECDN pagecdn.com -pagecdn.io -. -SCALAscala-lang.org scala-sbt.org -# -V8 -v8.dev v8project.org - -GIT git-scm.com -/ -ALIBABA alibaba -alipay -taobao tmall a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -adsalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.comalibabadns.com -alicdn.com alidayu.com -alidns.comaliyun-inc.comaliyun-iot-share.com -aliyun.com aliyuncdn.com aliyuncs.com hichina.comica-alliance.org mxhichina.com ddurl.to dingding.xin dingtalk.cn dingtalk.com dingtalk.netdingtalkapps.com laiwang.com lwurl.to -ele.me elemecdn.com eleme.cnteambition.comteambition.netteambitionapis.com mobmore.com  umeng.comumengcloud.com  umsns.com umtrack.com -uyunad.comwhalecloud.comalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads 25pp.com  56che.com  95095.com 9game.cn -aliapp.orgalibaba-inc.com alibaba.comalibabacapital.comalibabacorp.comalibabadoctor.comalibabafuturehotel.comalibabagroup.comalibabaplanet.comalibabaued.comalibabausercontent.com alifanyi.comalihealth.com.cn alihealth.hk -aliimg.com -alikmd.com alimama.com alimebot.com -alimei.com alios.cn -alipay.comalipayobjects.com aliplus.comaliresearch.com alisoft.com alisports.com alitianji.comaliunicorn.com -aliway.com aliwork.com alixiaomi.comalizhaopin.com alyms.cn -asczwa.comasczxcefsv.com -asmlink.cn atatech.org -b2byao.com  bazai.com  bcvbw.com  cheng.xin cloudeagle.cn damai.cn dayu.com -dfkhgj.com doctoryou.ai doctoryou.cn dongting.com -dratio.com emas-poc.com -gongyi.xin ialicdn.com iconfont.cn -ics.design -jhgtgb.com -kanbox.com -lazada.comliangxinyao.com m-now.cn -maitix.com  mdeer.com mei.commiaostreet.com  mshare.cc nic.xinphilanthropy.xin phpwind.com phpwind.netqingtingtrip.com  rexpos.cn saee.org.cn shenjing.com  shuqi.comshuqireader.com sparenode.com  supet.com  ttpod.com wandoujia.comxinlingshou.cn yowhale.com yunos-inc.com  yunos.comyushanfang.com -zuodao.comaliexpress.com aliexpress.ru sm.cn cainiao.comcainiao.com.cncainiao-inc.com freshhema.com hemamax.com -hemaos.com hemashare.cn shyhhema.com 1688.com etao.com juhuasuan.comlingshoujia.com -mashort.cn pailitao.com -taobao.com -taobao.org taobaocdn.comtaobizhong.comtaopiaopiao.com tb.cn tbcache.com tburl.in  tmall.com tmall.ru tmalltv.com tmjl.ai alitrip.com  feizhu.cn -feizhu.com -fliggy.com  fliggy.hk -iuynfg.com amap.com amapauto.com autonavi.com  gaode.com  xiami.com xiami.fm  xiami.net -+ -BOSLIFE boslife.biz boslife.net - -EPOCHMEDIAGROUP dajiyuan.com dajiyuan.euepochmediagroup.comepochstories.com epochtime.comepochtimes-romania.comepochtimes.co.ilepochtimes.co.krepochtimes.co.ukepochtimes.comepochtimes.com.auepochtimes.com.brepochtimes.com.sgepochtimes.com.twepochtimes.com.ua epochtimes.cz epochtimes.de epochtimes.eu epochtimes.fr epochtimes.it epochtimes.jp epochtimes.nl epochtimes.pl epochtimes.ru epochtimes.seepochtimestr.com epochweek.comepochweekly.com erabaru.netlagranepoca.com ntd.com ntdtv.ca ntdtv.co.kr  ntdtv.com ntdtv.com.tw ntdtv.jp ntdtv.kr  ntdtv.org ntdtv.ru ntdtvla.com  ntdvn.comtheepochtimes.comtheepochtimessubscribe.comvelkaepocha.cz -" - ALPINELINUXalpinelinux.org -i -ARCHIVEOFOUROWN ao3.orgarchiveofourown.orgarchiveofourown.comarchiveofourown.net - -AVSFORUM avsforum.com - -LUA lua.org - -PHP php.net - -WHOLEFOODSMARKETwholecitiesfoundation.orgwholefoodsmarket.comwholekidsfoundation.orgwholeplanetfoundation.org - -AOMEDIA aomedia.org -# -DCARD dcard.io dcard.tw - -LITV litv.tv - -BITBANK -bitbank.cc -2 - GROWINGIO-ADS!assets.growingio.com -ads - -VISA visa.be visa.ca visa.clvisa.cn -cn -visa.co.ao -visa.co.cr -visa.co.id -visa.co.il -visa.co.in -visa.co.jp -visa.co.ke -visa.co.ni -visa.co.nz -visa.co.th -visa.co.uk -visa.co.ve -visa.co.za visa.com visa.com.ag visa.com.ai visa.com.ar visa.com.au visa.com.az visa.com.bo visa.com.br visa.com.bs visa.com.bz visa.com.cn -cn visa.com.co visa.com.cy visa.com.dm visa.com.do visa.com.ec visa.com.ge visa.com.gt visa.com.gy visa.com.hk visa.com.hn visa.com.hr visa.com.jm visa.com.kh visa.com.kn visa.com.kz visa.com.lc visa.com.lk visa.com.ms visa.com.mx visa.com.my visa.com.ng visa.com.pa visa.com.pe visa.com.ph visa.com.pr visa.com.py visa.com.ru visa.com.sg visa.com.sv visa.com.tr visa.com.tt visa.com.tw visa.com.ua visa.com.uy visa.com.vc visa.com.vi visa.com.vn visa.cz visa.de visa.dk visa.es visa.fi visa.fr visa.gd visa.gp visa.gr visa.hk visa.hu visa.ie visa.is visa.ky visa.lt visa.lv visa.mn visa.mq visa.nl visa.no visa.pl visa.pt visa.ro visa.se visa.sk visa.so visa.sx visa.tc -visabg.com visaeurope.at visaeurope.ch visaeurope.lu visaeurope.sivisaitalia.com visakorea.comvisamiddleeast.comvisasoutheasteurope.comfinlitsummit.orgmyvisaluxuryhotels.comvisainfinite.cavisainfiniteluxuryhotels.ca!visaluxuryhotelcollection.com$ visaluxuryhotelcollection.com.br$ visaluxuryhotelcollection.com.mxvisaluxuryhotels.comvisaluxuryhotels.com.arvisasignaturehotels.com($connaissancesfinancierespratiques.ca dnsvisa.comeubluecardvisa.comfinancialsoccer.ca"financialsoccer.com.cn -cnlightbridge.commiamifintechfestival.commhshosting.compracticalbusinessskills.compracticalmoneyskills.ca#practicalmoneyskills.cn -cn'practicalmoneyskills.com.cn -cnpracticalmoneyskills.jpsoccerfinancier.catorcidadeouro.com vaultify.com vaultify.info vaultify.net visa-atm.com visa-news.jp visabank.orgvisabusinessinsights.comvisacarddesignlab.com visacards.comvisacheckout.netvisacheckout.orgvisaconciergelac.comvisadigital.comvisadigitalconcierge.com visadns.comvisadpsonline.usvisaeverywhereshop.comvisafintechfasttrack.comvisafulfillment.comvisagiftcard.usvisaglobalfinance.comvisaicsdirect.comvisainfinite.cavisainfinite.cn -cn visanet.net -visapay.cn -cnvisapcsdirect.comvisaplatinum.cn -cnvisaplatinum.com.cn -cn visaplus.comvisasavingsedge.cavisasignature.co.kr visb.org -* -DANGDANG dangdang.com ddimg.cn - - ZEROHEDGE zerohedge.com -+ - TAPPX-ADSssp.api.tappx.com -ads - -AIRWICK -airwick.at -airwick.be -airwick.ca -airwick.ch -airwick.cl airwick.co.in airwick.co.nz airwick.co.uk airwick.co.za airwick.comairwick.com.auairwick.com.brairwick.com.cn -cnairwick.com.mxairwick.com.tr -airwick.cz -airwick.de -airwick.dk -airwick.es -airwick.fi -airwick.fr -airwick.hr -airwick.hu -airwick.ie -airwick.it -airwick.jp airwick.net -airwick.nl -airwick.no -airwick.pl -airwick.pt -airwick.ro -airwick.ru -airwick.se -airwick.si -airwick.sk -airwick.tv -airwick.usairwickarabia.com vipoo.es - -DIDI didi-food.comdidiaustralia.blogdidichuxing.comdidiglobal.comdidimobility.co.jpdidistatic.comxiaojukeji.com - -EVERNOTE evernote.com - -ATOM atom.io - - CORDCLOUD cordcloud.org - -DISCORD -discord.co discord.com -discord.gg discord.gift discord.gifts discord.media discord.newdiscordapp.comdiscordapp.net -/ -FASTLANE fastlane.cifastlane.tools - -NVIDIA -geforce.cn -cn geforce.co.kr geforce.co.uk geforce.comgeforce.com.twgputechconf.cn -cngputechconf.co.krgputechconf.comgputechconf.com.augputechconf.com.twgputechconf.eugputechconf.ingputechconf.jp nvidia.asia  nvidia.be  nvidia.ch nvidia.cn -cn nvidia.co.jp nvidia.co.kr -nvidia.com nvidia.com.au nvidia.com.br nvidia.com.tr nvidia.com.tw  nvidia.cz  nvidia.de  nvidia.dk  nvidia.es  nvidia.eu  nvidia.fi  nvidia.fr  nvidia.it  nvidia.lu  nvidia.nl  nvidia.no  nvidia.pl  nvidia.ro  nvidia.ru  nvidia.seshotwithgeforce.com - -OPENSSL openssl.org - -RENZHE renzhe.cloud -* -SCIENTIFICLINUXscientificlinux.org -9 - SQUIRRELVPNsquirrelvpn.comsquirrelgroup.net - - WIREGUARD wireguard.com - -CEX -cex.io - -CUTTLY cutt.ly -J -HUPUhoopchina.com.cn hupu.com hupucdn.com  shihuo.cn -- -ASUS asus.com asus.com.cn -cn - -FIGMA  figma.com - -RUANMEI 6655.com cooldock.com -ithome.com lapin365.com ruanmei.com -saayaa.com win7china.com win8china.com -) -OSCHINA  gitee.com oschina.net -+ -RUBY ruby-lang.org rubygems.org - -DNSPOD dns.pub  dnsapi.cn  dnspod.cn -dnspod.com dnspod.com.cn -dnspod.net -dnspod.org  dnsv1.com  dnsv2.com  dnsv3.com  dnsv4.com  dnsv5.com doh.pub -" -KODI kodi.tv  kodi.wiki -r - LAUNCHPAD launchpad.netlaunchpadlibrarian.comlaunchpadlibrarian.netlaunchpadlibrarian.org - -PIGAV  pigav.com -: -BBCbbc  bbc.co.uk bbc.com -bbci.co.uk - -CNcn citic baidu -unicomsohu -xn--fiqs8s -xn--fiqz9s xn--1qqw23a xn--xhq521b xn--8y0a063a xn--6frz82g -xn--fiq64b xn--estv75g xn--vuq861b xn--zfr164b -42r.cn -5566ua.com aurorapush.cn -e0n.cn japps.cn jchat.io -jiguang.cn jmlk.co jpush.cn jpush.io jpushoa.com -jsurvey.cn  jvoice.cn -kc9.cn -n0q.cn pushcfg.com -s0n.cn mobmore.com  umeng.comumengcloud.com  umsns.com umtrack.com -uyunad.comwhalecloud.comalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads bootcss.com -bootcdn.cnphpcomposer.com -maoyun.commaoyuncloud.cnmaoyuncloud.comstaticfile.org qbox.me  qiniu.com qiniucdn.com qiniudn.com qiniudns.com  qnssl.com  ucloud.cn ufileos.com  aicdn.com upai.com upaiyun.com  upcdn.net  upyun.com  8686c.comchinanetcenter.com -wangsu.com ourdvsss.com yfcache.com -yfcalc.com yfcloud.com  yfdts.net  yfp2p.net -yfscdn.net yunfancdn.com -boc.cnbankofchina.combocaviation.com  boccfc.cn  bocgi.com bocgins.com  bochk.com bocichina.com bocigroup.com  bocim.combocservices.com.cn cignacmb.comcmb-leasing.com cmbchina.com cmbi.com.hkcmbwinglungbank.comicbc icbc-ltd.com icbc-us.com icbc.com.cn  95516.com unionpay.comunionpayintl.comunionpaysecure.com abchina.com  whccb.com cebbank.com ceblease.com -eb-ind.com ebasset.com ebchina.comebchinaintl.com -ebf.com.cn  ebfcn.com ebfortune.com ebtrust.com ebscn-am.com  ebscn.comeverbright.com  secec.comsunlife-everbright.com taikang.comtaikanglife.comtaikangzhijia.comtkhealthcare.comdanjuanfunds.com -xueqiu.com -58.com  58.com.cn 5858.com  58che.com 58xueche.com -anjuke.comanjukestatic.com chinahr.com -daojia.com diandao.org fang.com  sofun.com soufunimg.com  jxedt.com zhuancorp.comzhuanspirit.comzhuanzhuan.com c-ctrip.com colinker.com ctrip.cn ctrip.co.id ctrip.co.kr ctrip.co.th  ctrip.com ctrip.com.hk ctrip.my ctrip.sg ctripbiz.com ctripbuy.hk ctripteam.com doticloud.com easytrip.com hhtravel.comhhtravel.com.tw izuchebao.com  lvtds.com  shuntu.cn -skysea.com  suanya.cn -suanya.com -tieyou.com -toptown.cn toursbms.com trip.com tripcdn.com -vipdlt.com dangdang.com ddimg.cn -ele.me elemecdn.com eleme.cn  ganji.comganjistatic1.com3.cn  300hu.com -360buy.com 360buyimg.com -360top.com -7fresh.com baitiao.comblackdragon.com  caiyu.comchinabank.com.cn -dao123.comjcloud-cdn.comjcloud-live.comjcloud-oss.com -jcloud.comjcloudcache.com jcloudcs.com jclouddn.com jcloudec.com jcloudlb.comjcloudlive.com jcloudlv.com jcloudoss.com jcloudss.comjcloudstatic.comjcloudvideo.com  jclps.com -jd-app.com  jd-ex.com jd.cn jd.co.th -jd.com jd.hk jd.id jd.ru jdcache.com jdcloud.com jdcloudcs.comjdcloud-api.comjddapeigou.com jddebug.com jddglobal.com jdjinrong.com  jdpay.com jdpaydns.com jdx.com jdwl.comjingdongjinrong.com -jingxi.com -jkcsjd.com -joybuy.com  joybuy.eslinglonglife.com mayshijia.com minitiao.com  ocwms.com paidaojia.cn -paipai.comprestodb-china.com qianxun.comqingzhouip.com toplife.com -vg.com wangyin.com  wdfok.com yhd.com yihaodian.comyihaodianimg.com yiyaojd.com yizhitou.comads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads lianjia.com  ljcdn.com manmanbuy.com dianping.com -maoyan.com meituan.com meituan.net  mtyun.com  neixin.cn sankuai.com mogu.com mogucdn.com mogujie.com -pddpic.com pinduoduo.com yangkeduo.com  smzdm.com -zdmimg.com cnsuning.com -suning.comsuningcloud.comsuningestate.comsuningholdings.com 40017.cn -17u.cn 17u.net 17usoft.com 17usoft.net  elong.comelongstatic.com -ly.com  vancl.com vanclimg.com  vcimg.com -youzan.com yzcdn.cn youzanyun.com -ziroom.comziroomapartment.com 3dmgame.com 3304399.net 4399.com 4399api.com 4399dmw.com -4399er.com4399youpai.com 5054399.com img4399.com acfun.cn aixifan.com apilog-web.acfun.cn -ads -acplay.netdandanplay.com -ali213.net -acg.tv acgvideo.com -b23.tv biliapi.com biliapi.net bilibili.combilibiligame.net biligame.com biligame.net bilivideo.com  hdslb.com im9.com -mincdn.com$ upos-hz-mirrorakam.akamaized.net biji.com -ddmogo.com -igetget.cn igetget.com luojilab.com  mogoo.com -sao.cn saolife.com dmzj.com  douyu.com douyu.tv douyucdn.cn douyucdn2.cn douyuscdn.com douyutv.com gamersky.com -gcores.com gitv.cn gitv.tv huya.com msstatic.com 71.am  iqiyi.com iqiyipic.com -pps.tv -ppsimg.com qiyi.com qiyipic.com -qy.net -71edge.coma.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -ads  jjwxc.net ku6.com -ku6cdn.com -ku6img.com kkmh.comkuaikanmanhua.com gifshow.com ksapisrv.com kuaishou.com -yximgs.com log-sdk.gifshow.com -adswlog.kuaishou.com -ads  kgimg.com  kugou.com -le.com lecloud.com -lemall.com letv.com letvcdn.com letvimg.com miaopai.com -mihoyo.com bh3.com  pplive.cn -pplive.com pptv.com qingting.fm  sfacg.com -61.com -taomee.com -tapimg.com -taptap.com  tgbus.com tiancity.comtiancitycdn.com u17.com u17i.com u17t.com -vgtime.com ximalaya.com  xmcdn.com -kumiao.com  youku.com  ykimg.com -mmstat.com soku.com -cibntv.net -duowan.com dwstatic.com -yy.com yystatic.com -heytea.com chiphell.com -douban.com  douban.fm doubanio.com erji.net -hacpai.com -nga.cn 178.com -ngabbs.com ngacn.cc saraba1st.com  zhihu.com  zhimg.com  feng1.comrongechain.comsf-airlines.comsf-cityrush.comsf-express.comsf-financial.com sf-saas.comsf-tech.com.cn sfcar.hk sffix.cn sfgy.org 36kr.com 36krcdn.com  dgtle.comhoopchina.com.cn hupu.com hupucdn.com  shihuo.cn coolbuy.com  ifanr.com ifanr.inifanrusercontent.com -ifanrx.com -minapp.com -caixin.com cctv.cn cctv.com cctvpic.com chinaepg.cn chinalive.com citv.net.cn cnms.net.cn cntv.cn cntv.com.cn -cctvlib.cncctvlibrary.cncctvlib.com.cncctvlibrary.com.cn -cctvpro.cncctvpro.com.cn  cntvwb.cn gjgbdszt.cngjgbdszt.com.cngjgbdszt.net.cn  ipanda.cn -ipanda.com ipanda.com.cn -ipanda.net livechina.cn livechina.comolympicchannelchina.cn tvcc.cn tvcc.com.cn%!xn--fiq53l6wcx3kp9bc7joo6apn8a.cn%!xn--fiq53l90et9fpncc7joo6apn8a.cn-)xn--fiq53l6wcx3kp9bc7joo6apn8a.xn--fiqs8s%!xn--kprv4ewxfr9cpxcc7joo6apn8a.cn-)xn--kprv4ewxfr9cpxcc7joo6apn8a.xn--fiqs8s zggbdszt.cnzggbdszt.com.cnzggbdszt.net.cnzygbdszt.net.cn chinanews.comchinanews.com.cn -cns.com.cn ecns.cn geekpark.net jiemian.comjiemian.com.cn -lancdn.comlandiannews.com  wsoso.com  ifeng.com ifengimg.comphoenixcenter.cn fengedu.comifengweekly.com 6655.com cooldock.com -ithome.com lapin365.com ruanmei.com -saayaa.com win7china.com win8china.com sohu.com -sohucs.com -itc.cn solidot.org  sspai.com tmtpost.com chaindd.com -72whys.com checkzeit.com -tivitv.com tigeek.net.cntmtsmartlife.comtmtsmartrun.com feng.com fengimg.com wfdata.club ynet.cn ynet.com ynet.com.cnhainanairlines.com hnacargo.com  hnair.com 10044.cnsinopec-usa.com sinopec.comsinopecgroup.comsinopecnews.com.cnsinopecsales.com -csdl.ac.cn  las.ac.cn  fuwai.com pkuh6.cn puh3.net.cnhospital.pku.edu.cnss.bjmu.edu.cn 115.com 3type.cn 4paradigm.com  51job.com 51jobcdn.comyingjiesheng.comyingjiesheng.netzhiding.com.cn -afdian.net afdiancdn.com agora.io -agoraio.cn agoralab.co -antutu.com bitauto.combitautoimg.com cheyisou.com  yiche.com  yccdn.com shimo.im  yuque.com chinaso.com -pg0.cn -chinaz.com -chinaz.net caiyunai.com caiyunapp.com coolapk.comcoolapkmarket.comcoolapkmarket.net c-t.workcowtransfer.com geetest.com#geetest.datasink.sensorsdata.cn hellofont.cnhellowebfont.com0,hellofontpreview.oss-cn-beijing.aliyuncs.com-)hellowebfonts.oss-cn-beijing.aliyuncs.com hanyi.com.cnfontstar.com.cnfoundertype.com ipip.net jianshu.com -jianshu.io jinshuju.netjinshujucdn.com gotokeep.com keepcdn.com  lagou.com lgstatic.com baidupan.com -lanzou.com lanzous.com woozooo.com -liepin.com -lietou.comlietou-static.com mafengwo.cn mafengwo.com mafengwo.net  meitu.com meitudata.com moji.com mojichina.com  polyv.net videocc.net -renren.comrenren-inc.com vaptcha.com vaptcha.net wamawama.com -wiwide.com -sojump.com -wjx.cn wjx.com wjx.top zcmbc.com.cn zdns.cn zdns.net.cn zdns.org.cn zdnscloud.biz zdnscloud.cn zdnscloud.comzdnscloud.com.cnzdnscloud.info zdnscloud.netzdnscloud.net.cnzdnscloud.org.cnchinesemooc.org -cnmooc.orgicourse163.org icourses.cn mooc.cnopenlearning.com pmphmooc.commoocs.unipus.cnopencourse.pku.edu.cn study.163.com cnki.net  apabi.comcnthinkers.com  cqvip.comhuijistatic.com huijiwiki.com moegirl.orgmoegirl.org.cn  guokr.comwanfangdata.com.cn speiyou.com -xesimg.com xueersi.com banmaaike.comxiaoyuankousuan.com ybccode.com yuanfudao.com yuansouti.com yuantiku.com zhihuishu.com  coding.me -coding.net csdn.net -csdnimg.cn  deepin.cn -deepin.com  deepin.io -deepin.org  gitee.comjinrishici.com  juejin.imb-gold-cdn.xitu.iomirror-gold-cdn.xitu.iop1-juejin.byteimg.comp2-juejin.byteimg.comp3-juejin.byteimg.comp4-juejin.byteimg.comp5-juejin.byteimg.comp6-juejin.byteimg.comp7-juejin.byteimg.comp8-juejin.byteimg.comp9-juejin.byteimg.com oschina.netruby-china.orgrubyconfchina.org mysxl.cn -sxl.cn  sxlcdn.cnsegmentfault.com  sifou.com -tipdim.org alibaba -alipay -taobao tmall a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -adsalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.comalibabadns.com -alicdn.com alidayu.com -alidns.comaliyun-inc.comaliyun-iot-share.com -aliyun.com aliyuncdn.com aliyuncs.com hichina.comica-alliance.org mxhichina.com ddurl.to dingding.xin dingtalk.cn dingtalk.com dingtalk.netdingtalkapps.com laiwang.com lwurl.toteambition.comteambition.netteambitionapis.com 25pp.com  56che.com  95095.com 9game.cn -aliapp.orgalibaba-inc.com alibaba.comalibabacapital.comalibabacorp.comalibabadoctor.comalibabafuturehotel.comalibabagroup.comalibabaplanet.comalibabaued.comalibabausercontent.com alifanyi.comalihealth.com.cn alihealth.hk -aliimg.com -alikmd.com alimama.com alimebot.com -alimei.com alios.cn -alipay.comalipayobjects.com aliplus.comaliresearch.com alisoft.com alisports.com alitianji.comaliunicorn.com -aliway.com aliwork.com alixiaomi.comalizhaopin.com alyms.cn -asczwa.comasczxcefsv.com -asmlink.cn atatech.org -b2byao.com  bazai.com  bcvbw.com  cheng.xin cloudeagle.cn damai.cn dayu.com -dfkhgj.com doctoryou.ai doctoryou.cn dongting.com -dratio.com emas-poc.com -gongyi.xin ialicdn.com iconfont.cn -ics.design -jhgtgb.com -kanbox.com -lazada.comliangxinyao.com m-now.cn -maitix.com  mdeer.com mei.commiaostreet.com  mshare.cc nic.xinphilanthropy.xin phpwind.com phpwind.netqingtingtrip.com  rexpos.cn saee.org.cn shenjing.com  shuqi.comshuqireader.com sparenode.com  supet.com  ttpod.com wandoujia.comxinlingshou.cn yowhale.com yunos-inc.com  yunos.comyushanfang.com -zuodao.comaliexpress.com aliexpress.ru sm.cn cainiao.comcainiao.com.cncainiao-inc.com freshhema.com hemamax.com -hemaos.com hemashare.cn shyhhema.com 1688.com etao.com juhuasuan.comlingshoujia.com -mashort.cn pailitao.com -taobao.com -taobao.org taobaocdn.comtaobizhong.comtaopiaopiao.com tb.cn tbcache.com tburl.in  tmall.com tmall.ru tmalltv.com tmjl.ai alitrip.com  feizhu.cn -feizhu.com -fliggy.com  fliggy.hk -iuynfg.com amap.com amapauto.com autonavi.com  gaode.com  xiami.com xiami.fm  xiami.net  b3log.org a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -ads zuoyebang.cc zuoyebang.com -zybang.com baidu -91.com -aipage.com apollo.auto baidu.cn  baidu.com baidu.com.cn baidubce.combaiducontent.com baidupcs.combaidustatic.com -baifae.com baifubao.com -bcebos.com  bdimg.com bdstatic.com bdtjrcv.com  bdydns.cn -bdydns.com -bdydns.net chuanke.com  dlnel.com  dlnel.org -duapps.com -dwz.cn -hao123.com hao123img.com -hao222.com -haokan.com jomocdn.net jomodns.com -mipcdn.com  nuomi.com quyaoya.com smartapps.cn  tieba.com tiebaimg.com xianfae.com xiaodutv.com -bytecdn.cn bytedance.com bytedance.net bytedns.net byteimg.com -douyin.com huaxiong.comjokecommunity.comneihanshequ.com -pstatp.com -snssdk.com toutiao.comtoutiaocdn.comtoutiaocloud.comtoutiaohao.comtoutiaohao.net toutiaoimg.cntoutiaoimg.comtoutiaoimg.nettoutiaopage.com -wukong.comwukongwenda.cnwukongwenda.com zijiecdn.com zijiecdn.net  zjcdn.com  baike.com -ixigua.com ixgvideo.com  365yg.com didi-food.comdidiaustralia.blogdidichuxing.comdidiglobal.comdidimobility.co.jpdidistatic.comxiaojukeji.com  dbank.com dbankcdn.com harmonyos.com hicloud.com hihonor.com honor.cn -huawei.comhuaweicloud.com  vmall.com vmallres.com cmcm.com cmcmcdn.com  liebao.cn duba.com duba.net ijinshan.comseasungames.com xishanju.com xoyo.com testplus.cn -wps.cn  51dzt.com  iciba.com  docer.com kdocs.cn papocket.com  wpspdf.cn kimg.cn kingsoft.com ksmobile.com -ks-cdn.com  ksyun.com ksyungslb.com flyme.cn meizu.cn  meizu.com  mzres.com 126.com 126.link 126.net 127.com 127.net 163.com163industry.com 163mail.com 163qiyukf.com -163yun.com 166.net -haowu.linkicourse163.org  kaola.com -lofter.com  nease.netnetease-inc.com netease.com -netease.im -qiyukf.com ydstatic.com yeah.net -youdao.comanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads coloros.com finzfin.com -heytap.com heytapcs.comheytapdownload.comheytapimage.com -myoppo.com nearme.com.cn oppo.cn oppo.com  oppo.mobioppodigital.com  oppoer.meoppomobile.com oppopay.com opposhop.cn -realme.com realmebbs.comrealmemobile.com oneplus.com -oneplus.cn oneplus.net opstatics.comoneplusbbs.com h2os.com 75.team -75team.com baomitu.com -360.cn 360.com -360-jr.com 360chou.com360daikuan.com360huzhubao.com -360jie.com360jinrong.net -360kan.com 360kuai.com  360os.com 360panyun.cn 360safe.com 360shouji.com 360taojin.com360totalsecurity.com 360tpcdn.com360webcache.com anquanke.com  haoso.com -haosou.com haosou.com.cn -haosou.net i360mall.com ludashi.com nicaifu.com  nzwgs.com  qhimg.com qhimgs0.com qhimgs1.com qhimgs2.com qhimgs3.com qhimgs4.com qhimgs5.com qhimgs6.com  qhmsg.com  qhres.com qhstatic.com qhupdate.com -qihuapi.cn qihucdn.com  qihoo.com qikucdn.com -so.com sou.com urlqh.cnxiaoluyouxuan.comxiaoluzhidian.com xjietiao.com -youhua.com  yunpan.cn  97973.com -picself.cn sina.cn sina.com sina.com.cn sina.net sinaapp.com sinaedge.com -sinaimg.cn sinaimg.com  sinajs.cn -sinajs.comt.cn videoself.cn  wbimg.com wbimg.cn wcdn.cn weibo.cn  weibo.com weibo.com.cn weibocdn.com xhaiwai.com 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -ads -go2map.com sogo.com  sogou.com sogoucdn.com dns.pub  dnsapi.cn  dnspod.cn -dnspod.com dnspod.com.cn -dnspod.net -dnspod.org  dnsv1.com  dnsv2.com  dnsv3.com  dnsv4.com  dnsv5.com doh.pub ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads -yuewen.com hongxiu.com lrts.me qdmm.com -qidian.com readnovel.com tingbook.com webnovel.com -xs8.cn xxsy.net -apcdns.net  cdn-go.cn cdntips.com foxmail.comfoxmail.com.cn gtimg.cn  gtimg.com gtimg.com.cn idqqimg.com imqq.com  myapp.com myqcloud.com -qcloud.com qcloudimg.com qlogo.cn qpic.cn -qq.com -qqmail.com  qzone.com -rtx.com.cnservicewechat.com smtcdns.com smtcdns.net -tencent.cn tencent.comtencent.com.cntencent.com.hktencent.net.cntencent-cloud.comtencentmusic.comtencentyun.com -tenpay.com -wechat.com -wegame.com wegame.com.cn -weiyun.com alloyteam.com imweb.io ivweb.io -mi.com -mi-img.com  mifile.cn miui.com -miwifi.com  xiaomi.cn -xiaomi.com -xiaomi.netxiaomiyoupin.com  00cdn.com  88cdn.com -sandai.netthunderurl.com -xunlei.com 10086.cn 139.comchinamobile.comchinamobileltd.com migucloud.com migu.cn -cmvideo.cn miguvideo.com andfx.cn  andfx.net -cmicrwx.cncmpassport.comfetion-portal.com fetionpic.com mmarket.com mmarket6.com -ctexcel.ca ctexcel.comctexcel.com.hk -ctexcel.fr -ctexcel.us -189.cnchinatelecom-h.comchinatelecom.com.cnchinatelecomglobal.com -chntel.com  10010.com 10010.com.cnchinaunicom.comchinaunicom.com.cnchinaunicom.com.hkchinaunicomglobal.commychinaunicom.com  wo.com.cnguoxuedashi.com guoxuemi.com jikipedia.com haidilao.com0123456789.com 0379home.com -055110.com  05sun.com 0759job.com  100bt.com 100offer.com  11467.com -114chn.com 114piaowu.com 114shouji.com -119you.com 123juzi.com 123juzi.net 123u.com 1637.com  16888.com 16rd.com  17173.com 178linux.com  17989.com  17cdn.com 17house.com  18183.com 188.com 1905.com  19yxw.com 1nongjing.com  1ting.com 1zhe.com 21cn.com 2265.com 2288.org 2345.com 258.com 263.net 289.com 2cto.com 3000.com -310win.com -315che.com 3234.com 32r.com 3322.org 3387.com 33lc.com 3454.com 3456.cc -35.com 3533.com 360aiyi.com -360doc.com  360jq.com 360zhyx.com -365azw.com -365dmp.com -37.com 3733.com 3839.com -39.net 3conline.com 3d66.com -3elife.net 3h3.com 3yx.com  40407.com 500.com 500d.me -50bang.org -51.com 51.la -510you.com 51credit.com  51cto.com 51eshop.com 51gaifang.com 51hejia.com51jingying.com 51meishu.com -51sole.com 51wendang.com  51yes.com 5253.com  52hxw.com -52miji.com 52pk.com 52z.com 53kf.com -55.com -56.com 5577.com  55bbs.com  58pic.com  5bite.com 5fun.com 5gzm.net 5khouse.com 5you.com 66wz.com 68h5.com -6rooms.com 714.com 7230.com -72byte.com  7down.com 7k7k.com  7moor.com 7po.com -7x24cc.com -7xdown.com  818ps.com  84399.com 885.com -900.la 917.com 91danji.com 91yunxiao.com 9553.com 962.net 9718.com  99166.com 998.com 999d.com 99danji.com 9ht.com -9xgame.com 9xu.com a9vg.com -aardio.com -adxvip.com -afzhan.com agrantsem.com  aiemy.com aiketour.com  aiops.com  aipai.comairchangan.com airguilin.com aisee.tv aiskycn.com aiyunxiao.com -aizhan.comalittle-tea.com -allyes.com anfensi.com -anquan.org -anruan.com -aotrip.net  aoyou.com apk3.com apk8.com appchina.com -appinn.com  arpun.com babytree.combabytreeimg.com baidufree.combaifendian.com  baihe.com baixing.com baixing.netbamenzhushou.combangongziyuan.com bankcomm.com baofeng.combaoxianshichang.com bcy.net bearychat.com beihaidc.combeijing-lipin.com bengbeng.com  benmi.com  berui.combestopview.com biancheng.net bianews.com bibaodao.com bidchance.com biddingx.com bihu.com -bio360.net  bio4p.com bio-equip.combiodiscover.com bishijie.com bitecoin.com biyehome.net bjcathay.com bobo.com bojianger.com -bokecc.com  boosj.com btcfans.com btgtravel.com bthhotels.com  btime.comcabbagebox.com caijingwu.com -caiyun.com cang.comcankaoxiaoxi.comcardbaobao.com -carnoc.com ccb.com  cdn20.com  cecdc.com cecport.com centanet.com cerambath.org -ch.com chachaba.com changba.com chashebao.comchazhengla.com -che168.comchina-chair.comchina-nengyuan.com  china.comchinacache.comchinacache.netchinachugui.comchinadongchu.com chinafph.comchinakaoyan.comchinalawedu.com chinapay.com chinapp.netchinapubmed.net chinasspp.comchinatranslation.net chinaunix.netchinaweiyu.comchinawutong.com chinazns.comchongdiantou.com -chouti.com chuangxin.com chuansong.mechuansongme.com -chushou.tv chuyu.me  city8.com cjol.com clouddn.com cloudxns.com cnanzhi.com -cnbeta.com cnbetacdn.com cnblogs.com -cnepub.com  cnfol.com  cnlaw.net cnstock.com cnzz.com cocodiy.com code666.com -cofeed.com -cofool.com coindog.comcontentchina.com  cpppf.org  cr173.com  crsky.com  csbew.com -csmjzs.com csslcloud.net -ctfile.com ctiforum.com ctoutiao.com  ctsbw.com -cubead.com  cwxzx.com -cyagen.com  cybtc.com cyol.com cyts.com cztv.com cztvcloud.com dadajiasu.com dadighost.com daguzhe.com dajiabao.com  dajie.com dajieimg.com dalongyun.com dangbei.com danji100.com -danji6.com daocloud.io daovoice.iodayukaoshi.com dazijia.com -dcdkjx.com  dd373.com  ddooo.com dedecms.com develenv.com -dginfo.comdgphospital.comdianchacha.com diandian.comdiandianqi.comdianwannan.comdingxiang-inc.com -diopic.net diyiyou.com dj99.com  docin.com dockerone.com -dockone.io dogedoge.com -donews.com -dongao.comdongguantoday.com dopa.com -douxie.com -downcc.com downxia.com -downyi.com -dpfile.comdreams-travel.comdrivergenius.com -dui.ai duiopen.com duoduocdn.com duokanbox.com -duomai.com duoshuo.com  duote.com dxpmedia.com -dxycdn.com  dyhjw.com dzsc.com dzw3.com  dzwww.com easesale.com  easiu.com eastday.com eastmoney.com eastsoo.com -eayyou.com  ebico.com  ebrun.com -ecitic.com ecjobhome.com -edu.cn  eduei.com eeyy.com eflycloud.com elecfans.com emarbox.com -emumax.com enterdesk.comeoeandroid.com excelhome.net expreview.comf.cx  faloo.com famulei.com  fanli.comfangxiaoer.com fanxian.com fastapi.net -feihuo.com feiniaomy.comfengkongcloud.com fengniao.comfengxian110.com  fenxi.com -fhldns.com  fimmu.com finchina.com fixdown.com  fqjob.net freebuf.com fugetech.com fuwj.com fxxz.com fyxz.com game234.com gameabc.com gamemei.com -gaodun.comgaoshouyou.comgaoxiaojob.com -gdjdxy.comgdliontech.com gdrc.com geilicdn.comgenelinkout.com  getui.com -gezida.com gfan.com -giocdn.com globrand.com gm86.com gmallbio.com  gmz88.comgobasearcher.com gold678.com  golue.comgongxiangcj.com goosail.com  goufw.com gree.comgreenxiazai.com growingio.com  gtags.net  guabu.com guaiguai.comguanaitong.comguanhaobio.com gucheng.com  guigu.org guoxinmac.com  gupzs.com gushiwen.orgguzhangting.com -gwdang.comgxairlines.com -gxb.io gzwanju.com h5uc.com hack520.com hackhome.com haishun.com hanboshi.com handanjob.comhangjiayun.com haolietou.com  haote.comhaoyouyinxiang.com hefei.cc -heisha.net  henha.com henkuai.com herostart.com  hexun.com  hiido.com hitv.com hiyd.com hktheone.com hn-cbec.com -hnrcsc.com hntv.tv hnyuyuhui.com hollycrm.com homeinns.com homekoo.comhongsanban.com -houdao.comhouyuantuan.com hteacher.net huanqiu.comhuashengdaili.com  huatu.com  huize.com  hujia.org hujiang.com hunantv.comhuochepiao.com huocheso.com  huxiu.com hxfy888.com hxsd.com iask.com -ibaotu.com ibuychem.com icaidao.com ichangtou.com idongdong.com iflytek.com  igao7.com -iheima.com ijiandao.com -ikafan.com im2maker.com  im-cc.com -imedao.com imgo.tv  imooc.com indienova.com  infoq.cominfoqstatic.com -ip-cdn.com  ip138.com ipinyou.com iranshao.com  irs01.com ishumei.com  it165.net  it168.com  it610.com  iteye.com -itjuzi.com  itmop.com  iyiou.com iyunxiao.com -izuche.com -jandan.net jb51.com  jdair.net jia.com jiameng.com jianbihua.com jianzhi8.com jiaodong.net jiaoyimao.com jiasuhui.com jiasule.com jiathis.com jiayuan.com jiazhao.com jiemeng8.comjikexueyuan.com jinianbi.comjinrongren.net  jinse.com  jinti.comjiqizhixin.com jisuanke.com jisuxia.com jiuzhilan.com jj20.com jkbl.com jmstatic.com job5588.com -job910.com  jobjm.com jsbc.com -jsrdgg.com jstv.com  jsyks.com juefeng.com -julive.com  jumei.com  juwan.com  juxia.com  jyimg.com k366.com kaifubiao.com kaixin001.com kameng98.com -kanimg.com kankanews.com kantianqi.net kanzhun.com kaopu001.comkeboyunxiao.com  kejet.net kf5.com -kimiss.com kingdee.com kkzj.com -kongfz.com koolearn.com -kouclo.com -koudai.com koudai8.com kq7.com  ksord.com  kuaaa.com kuaidi100.com kuaihou.com kuaizhan.com  kuqin.com -kuyiso.com lady8844.com laomaotao.net laomaotao.org  lascn.net  lawxp.com leanote.com ledu.comleetcode-cn.com leikeji.com leiphone.com leiting.com leju.com leturich.org lianmishu.com -liantu.com  liantu.me lianzhuli.comliaoxuefeng.com liba.com libaclub.comlieyunwang.com lightonus.com -likuso.com lingkou.com linkvans.com  linsn.com linuxidc.com liulanqi.net liuxiaoer.com live800.com liweijia.com lixinger.com lkkbrand.com longhoo.net -longre.comlongtugame.com longzhu.com  lotpc.com -loupan.com -lu.com luckyair.net  lufax.com lufaxcdn.com lunwengo.net luyouqi.com -lvmama.comlvsexitong.com  lxdns.com  lxway.com -magedu.com maisanqi.com -mamecn.com manhuatai.com manmankan.com -mapbar.com mayi.com -mayihr.com -mcbang.com  mdpda.com mechina.org -mediav.com meifang.com meika360.com meilishuo.commeimingteng.com -meiqia.commeishichina.com meishij.netmengtuoshi.wang mgtv.commiaobolive.com miaozhen.com -miercn.com mikecrm.comminhangshi.com miusi.co mjia.cc -mmbang.com mmbang.info -mofang.com mofazhu.com mogu.com mogucdn.com mogujie.com -mokahr.com moleecel.commoocollege.com mop.com msanjia.com -mscbsc.com mukewang.com -mumayi.com -muomou.com muzhiwan.com -mydown.com mydrivers.com myshow360.net myyx618.com myzaker.com naixuecha.com netbian.com -newasp.netnewbandeng.com  newyx.net nowcoder.com nowscore.com  nruan.com ntalker.com nvsheng.com  nzbdw.com oadz.com  oeeee.com  offcn.com officese.com -oicq88.com  okooo.com -ol-cdn.com -ol-img.com oldboyedu.com -oneapm.com onegreen.net -oneyac.comonlinedown.netonlinesjtu.com onlylady.com -opdown.com oray.com  ottcn.com -oupeng.com -p2peye.com p5w.netpaipaibang.com paopaoche.net pc6.com -pcbeta.com -pchome.net  pcpop.com  peccn.com pgzs.compharmacodia.com piaoliang.com -pingan.compingplusplus.com pingwest.com -pingxx.com pintu360.com  pipaw.com  pipaw.net pptbest.com psbc.com -pubyun.com  qc188.com qcc.com qcds.com qd8.com qd-metro.com qh24.com qiangbus.com qianjia.com qianlima.com qianzhan.comqiaobutang.com qichacha.com -qie.tv qifeiye.comqijiayoudao.net qingcloud.comqingdaochina.orgqingdaonews.comqingsongchou.com qiumibao.com qizuang.com  qqddc.comqqgexingqianming.com qqrizhi.comqqshuoshuo.com -qschou.com -qtmojo.com -qudong.com -qugame.com  qunar.com qunarzz.com qyer.com rfchost.comrockyenglish.com rong360.com rtbasia.com  ruan8.com -runoob.com rzrc114.com  saibo.com sanguosha.com -sanwen.net  scdng.com scoregg.com -sczsxx.com sdo.com sec-wiki.com -sencdn.com seniverse.comsenlinjimore.com -sgamer.com shang360.com -shangc.netshanghairc.com shangzhibo.tvshanshuihotel.com sharejs.com shebiaotm.comshelter-china.comshelterdome.netshengdaosoft.comshenmayouxi.com  sheui.com shijiexia.com shixiseng.com shmetro.comshoujihuifu.comshouyoubus.com shrbank.comshuidichou.comshuidihuzhu.com shumensy.comshuoshuokong.com -shutcm.com -simei8.com simuwang.com sinesafe.com  sjwyx.com  skycn.com sllssrq.com -snimay.com  sobot.com sofreight.com softwhy.com -sojson.com -somode.com soso.com souqian.comsouthmoney.comsparkletour.comspringtour.com ssports.com starbaby.cc starbaby.com stockstar.com sun0769.com -sunrtb.com supermap.comsupermapcloud.com -sys321.com szfw.org szhk.com szzfgjj.com t1y5.com  taihe.comtaihuoniao.com tangeche.com tanx.com -tao123.com -taoche.com te5.com tenxcloud.comtianjimedia.comtianjin-air.comtianlailive.com -tianqi.com tietuku.com -tiexue.net timedoo.com tingyun.com titanar.com tjjt360.com  tmcdn.net -tobosu.com tom.com tongdun.net topthink.com touqikan.com travelsky.com trunktech.comtrustexporter.com trustutn.org  tudou.com tudouui.com tuer123.com tuicool.com -tuiwen.net  tujia.com  tuniu.com tutuapp.com  tuwan.com tuwanjun.com u.tools  ucbug.com -uggame.com uoko.com urselect.comurumqi-air.com  useso.com uuu9.com uzzf.com  v5875.com value500.com vamaker.com vdolady.com veryhuo.com veryzhun.com  vic18.com videojj.com  vijos.org vip.com  vobao.com -vpgame.comwallstreetcn.comwangyangyang.vipwannianli8.com wanplus.com wanwushuo.com wanyiwang.com  wanyx.com -wdjimg.com wdzj.com  weand.com webterren.com weicaifu.com weidian.comweilaicaijing.comweilaiyunxiao.com weiphone.net weixing.com weiyangx.comwelltrend-edu.com wenjuan.comwhlovehome.com wicresoft.com -win007.com win8.netwindows10zj.comwindows7en.com windowszj.com winwin7.com wishdown.com  wmzhe.com wnhuifu.com wodingche.com  womai.com wonnder.com -wooyun.org worktile.comwoyaogexing.com  woyoo.com  wqiis.com wrating.com -wscdns.com  wtown.com  wumii.com -wuming.comwuximediaglobal.com  wywyx.com xafc.com  xbiao.com  xdwan.com -xfdown.com -xhscdn.comxiachufang.comxiaobaixitong.comxiaoe-tech.com xiaogouh5.comxiaohongshu.com  xiaoka.tv xiaokaxiu.comxiaomingtaiji.comxiaoxiongxitong.comxiaoyuxitong.com  xiayx.com xiazaiba.com xingjiesj.com xinhuanet.comxinrenxinshi.com xinshipu.comxingtan001.com xitong8.com xitu.com xiu8.com -xiucai.com xiziwang.net xmhouse.com  xnpic.com  xpgod.com -xsa239.comxuanchuanyi.com  xue51.com xuekanba.com xuexicha.com xundupdf.com  xycdn.com xywy.com -xzking.com yahui.cc -yaolan.com  yaoyl.com  yaozh.com -ycgame.com -yeepay.com -yeshen.com  yesky.com yh31.com  yicai.com  yigao.com yigoonet.com yihedoors.comyikaochacha.com yikexue.comyingjia360.comyinhang123.net yinxiang.com yinyuetai.comyinzhaowang.com -yiqifa.com -yiqifa.org yisu.com yitaifang.com  yiwan.com yixi.tv yizhibo.com  yjbys.com ynhr.com -yocajr.com -youboy.com youjiao.com youke.co youlian.fun youshang.com youxiake.com youxihezi.net youxila.com youxiniao.com youxigt.com  youzu.com yoyojie.com  yoyou.com  ypppt.com -yslyhr.com  ysten.com -yuewen.com  yunaq.com yunba.io yundaex.com yundasys.com -yundun.com yunqishi.net yunshipei.com yunxiao.com yunzhijia.com  yupoo.com  yuzua.com  yxbao.com -yxdown.com -yytcdn.com -zampda.net zampdsp.com zastatic.com -zbjimg.com zczj.com -zdfans.com zgjm.org -zgjsks.com  zglxw.com  zgzcw.com zhanbuba.comzhangyoubao.com  zhanqi.tv zhanzhang.net zhaodanji.com zhaopin.com zhcw.com -zhenai.com  zhibo8.cc  zhidx.com zhifang.com -zhipin.com zhiziyun.com zhongkao.comzhuangjiba.comzhuanyewanjia.comzhuayoukong.com zhubajie.com zhugeapi.net zhugeio.com  zimuzu.io zixiaomao.com zixuntop.com  zjstv.com zmengzhu.com zongheng.com -zpb365.com -zrblog.net zsxq.com  zuche.com zuchecdn.com zuhaowan.com zuidaima.com zuowen8.comzuowenwang.net jindidata.comtianyancha.comzhuanxiandai.com -fm3838.comlarsonlimited.com runsheng.com vzuu.com -zoom3g.com -zoomyou.cn chaoxing.com chaoxing.cc -O -COSTCOcostco-static.com -costco.comcostcobusinessdelivery.com - -DGTLE  dgtle.com -" - FOUNDERTYPEfoundertype.com -I -PYTHON -python.orgpythonhosted.org pypa.io pypi.org -/ -REALTYPErealtype.co.jp realtype.jp -s -SINOPECsinopec-usa.com sinopec.comsinopecgroup.comsinopecnews.com.cnsinopecsales.com -' - AMERICASVOICEamericasvoice.news -g - ATLASSIAN atlassian.com bitbucket.org statuspage.io -trello.com trellocdn.com - -BINANCEbinance.charity binance.cloud binance.combinance.vision binancezh.com bnbstatic.com yshyqxx.com -) -MEITU  meitu.com meitudata.com - -HINET  hinet.net - - -JAVLIBRARYjavlibrary.com -Y -JFROG  jfrog.com  jfrog.org bintray.com!d29vzk4ow07wi7.cloudfront.net -0 - COWTRANSFER c-t.workcowtransfer.com -' -FOXfoxfox-corporation.com fox-news.com fox.com -fox.tv fox10.tv fox10news.comfox10phoenix.com  fox11.comfox13memphis.com fox13news.com  fox23.comfox23maine.com -fox247.com  fox247.tv  fox26.comfox26houston.comfox28media.com  fox29.comfox2detroit.com fox2news.com  fox32.comfox32chicago.comfox35orlando.comfox38corpuschristi.com fox42kptm.com  fox46.comfox46charlotte.com  fox47.com fox49.tv fox4news.com fox51tns.netfox5atlanta.com -fox5dc.com -fox5ny.com fox5storm.com fox6now.com fox7.comfox7austin.com fox9.comfoxacrossamerica.comfoxaffiliateportal.comfoxandfriends.com -foxbet.comfoxbusiness.comfoxbusiness.tvfoxbusinessgo.comfoxcanvasroom.comfoxcareers.comfoxcharlotte.com foxcincy.com foxcincy.jobs foxcincy.netfoxcollegesports.comfoxcorporation.comfoxcreativeuniversity.com foxcredit.com foxcredit.org foxd.tv -foxdcg.comfoxdeportes.comfoxdeportes.netfoxdeportes.tvfoxdigitalmovies.com foxdoua.comfoxentertainment.com -foxest.com -foxfaq.com -foxfdm.com foxfiles.com -foxinc.com foxkansas.com  foxla.com foxla.tvfoxlexington.comfoxmediacloud.com foxnation.comfoxnebraska.com -foxneo.comfoxneodigital.comfoxnetworks.infofoxnetworksinfo.com -foxnews.cc foxnews.com foxnews.net foxnews.org -foxnews.tvfoxnewsaffiliates.comfoxnewsaroundtheworld.comfoxnewsb2b.comfoxnewschannel.com foxnewsgo.net foxnewsgo.org foxnewsgo.tvfoxnewshealth.comfoxnewslatino.comfoxnewsmagazine.comfoxnewsnetwork.comfoxnewsopinion.comfoxnewspodcasts.comfoxnewspolitics.comfoxnewsradio.comfoxnewsrundown.comfoxnewssunday.com  foxon.comfoxphiladelphia.com foxpoker.com  foxrad.io foxredeem.comfoxrelease.comfoxrichmond.com foxrobots.comfoxsmallbusinesscenter.comfoxsmallbusinesscenter.netfoxsmallbusinesscenter.org foxsoccer.net foxsoccer.tvfoxsoccermatchpass.comfoxsoccerplus.comfoxsoccerplus.netfoxsoccerplus.tvfoxsoccershop.comfoxsports-chicago.comfoxsports-newyork.comfoxsports-world.com foxsports.cl foxsports.cofoxsports.co.ve foxsports.comfoxsports.com.arfoxsports.com.bofoxsports.com.brfoxsports.com.cofoxsports.com.ecfoxsports.com.gtfoxsports.com.mxfoxsports.com.pefoxsports.com.pyfoxsports.com.uyfoxsports.com.ve foxsports.gtfoxsports.info foxsports.netfoxsports.net.br foxsports.pe foxsports.sv foxsports.uyfoxsports2.comfoxsportsflorida.comfoxsportsgo.comfoxsportsla.comfoxsportsnetmilwaukee.comfoxsportsneworleans.comfoxsportsracing.comfoxsportssupports.comfoxsportsuniversity.comfoxsportsworld.comfoxstudiolot.com foxsuper6.com -foxtel.com foxtel.com.aufoxtelevisionstations.com  foxtv.com foxtvdvd.com  foxuv.comfoxweatherwatch.com  fssta.com -fxn.ws fxnetwork.comfxnetworks.com bentobox.tv kicu.tv ktvu.commyfoxsanfran.comafewmomentswith.com -anidom.comcasoneexchange.comcoronavirusnow.com -fse.tvgeraldoatlarge.comgooddaychicago.com joeswall.comkilmeadeandfriends.commaskedsingerfox.com -my13la.com -my20dc.commy20houston.com -my29tv.com my45.com  my9nj.commyfoxatlanta.commyfoxaustin.commyfoxboston.commyfoxcharlotte.commyfoxchicago.com myfoxdc.commyfoxdetroit.com myfoxdfw.commyfoxhouston.commyfoxhurricane.com myfoxla.commyfoxlosangeles.commyfoxlubbock.commyfoxmaine.com myfoxny.commyfoxorlando.commyfoxphilly.commyfoxphoenix.commyfoxtampa.commyfoxtampabay.commyfoxtwincities.com myfoxzone.com myq2.comnewsnowfox.comorlandohurricane.comparadisehotelquizfox.com q13.com -q13fox.comrealamericanstories.comrealamericanstories.inforealamericanstories.netrealamericanstories.orgrealamericanstories.tvrealmilwaukeenow.com rprimelab.comshopspeedtv.comsoccermatchpass.comspeeddreamride.comspeedfantasybid.comspeedracegear.com speedxtra.comteenchoice.com testonfox.comtheclasshroom.comthefoxnation.comthegeorgiascene.comwhatthefox.comwhosthehost.com wofl.tvwoflthenewsstation.com wogx.com foxnewsplayer-a.akamaihd.net - - HUNANTV-ADSclick.hunantv.com -adsda.hunantv.com -ads da.mgtv.com -adslog.hunantv.com -adslog.v2.hunantv.com -adsres.hunantv.com -adsv2.log.hunantv.com -ads - -WEBTYPE webtype.com -< -DOCKERcompose-spec.io -docker.com  docker.io -' -MOJI moji.com mojichina.com -2 -MORISAWAmorisawa.co.jptypesquare.com - - - MICROSOFT-DEVadaptivecards.ioapi-extractor.com -apisof.net appcenter.ms -blazor.netbotframework.comcodethemicrobit.comdevopsassessment.net dot.netgamesstack.comgraphengine.ioinsiderdevtour.com -jwt.ms microbit.orgmicrosoftadc.commicrosoftgamestack.commicrosoftiotinsiderlabs.commicrosoftreactor.cn -cn#microsoftreactor.com.cn -cnmicrosoftreactor.infomicrosoftreactor.netmicrosoftreactor.orgmicrosoftsilverlight.commicrosoftsilverlight.orgmicrosoftsilverlight.netmicrosoftsqlserver.com  mmdnn.commono-project.com msdn.commsinnovationchallenge.commspairlift.commsropendata.com  nuget.org nugettest.orgopentranslatorstothings.orgposhtestgallery.compwabuilder.comreactorms.com.cn -cn -sankie.netsqlserveronlinux.comtimelinestoryteller.comuwpcommunitytoolkit.comvisualstudio-staging.comvisualstudio.comvisualstudio.covisualstudio.co.ukvisualstudio.euvisualstudio.net vfsforgit.com vfsforgit.org vsassets.io xamarin.comdeveloper.microsoft.comdtlgalleryint.cloudapp.net poshtestgallery.cloudapp.net"psg-int-centralus.cloudapp.netpsg-int-eastus.cloudapp.net - -AP -ap.org -apnews.com - -BINGbing bing.com bing.com.cn -cn bing.net bingads.combingagencyawards.combingapistatistics.combingsandbox.combingvisualsearch.com bingworld.combluehatnights.com -dictate.msflipwithsurface.commasalladeloslimites.commicrosoft-give.commicrosoftcloudsummit.commicrosoftdiplomados.commicrosoftlatamholiday.commicrosoftmxfilantropia.com!microsoftpartnersolutions.commsunlimitedcloudsummit.comoffice365love.comoffice365tw.comrenovacionoffice.comsprinklesapp.com cn.bing.com -cn cn.bing.net -cn ditu.live.com -cnbj1.api.bing.com -cnemoi-cncdn.bing.com -cn -. -JIEMIAN jiemian.comjiemian.com.cn - -JIGUANG -42r.cn -5566ua.com aurorapush.cn -e0n.cn japps.cn jchat.io -jiguang.cn jmlk.co jpush.cn jpush.io jpushoa.com -jsurvey.cn  jvoice.cn -kc9.cn -n0q.cn pushcfg.com -s0n.cn -" - MASTERCLASSmasterclass.com - -WEVERSE -weverse.io -. -NETLIFY netlify.combitballoon.com -{ -OREILLY oreil.ly oreilly.comoreilly.com.cn -cnoreilly.revieworeillystatic.com -orl.ly -1 -GODADDY godaddy.comx.co godaddy - -DENO  deno.land -@ -REUTERS reuters.com -reuters.tvreutersmedia.net -4 -SCInewisiknowledge.comisiknowledge.com - -BLUBRRY blubrry.com -3 - -STREAMABLE embed-cdn.comstreamable.com - -WESTERNDIGITALg-technology.com -sandisk.ae -sandisk.it -sandisk.cn -cn sandisk.co.jp sandisk.co.kr sandisk.co.uk sandisk.comsandisk.com.ausandisk.com.brsandisk.com.trsandisk.com.tw -sandisk.de -sandisk.es -sandisk.fr -sandisk.hk -sandisk.id -sandisk.in -sandisk.nl -sandisk.sg -wd.com wdc.comwesterndigital.com - - CATEGORY-ORGS aomedia.org archive.org  atypi.org -change.org ccstatic.orgcreativecommons.orgcreativecommons.engineeringlicensebuttons.net csis.orgcsis-prod.s3.amazonaws.comemojipedia.orgworldemojiawards.comworldemojiday.com xn--yt8h.laglobalsecurity.org ieee.org ieee.tvtryengineering.org ieee-ccnc.org ieee-ecce.org ieeemce.orgstandardsuniversity.orgieee-region6.org ieee.ca ieeer10.org -ieeer5.org -ieeer8.org ieeeusa.orgieeefoundation.orgieee-into-focus.org ieee-vics.org ieeeday.orgdatasheets360.comglobalspec.com ieee-isto.orgtechstreet.com computer.org -comsoc.org embs.org emcs.org grss-ieee.org ieee-aess.org ieee-cas.org ieee-ceda.orgieee-edusociety.org ieee-ies.org ieee-ims.org ieee-npss.org ieee-pels.org ieee-pes.org ieee-ras.orgieee-sensors.org ieee-tems.org ieee-uffc.org ieeeaps.org ieeecsc.org ieeecss.org ieeedeis.orgieeemagnetics.org ieeenano.org ieeeoes.org ieeesmc.orgieeesystemscouncil.org  itsoc.org mtt.orgphotonicssociety.orgsignalprocessingsociety.orgtechnologyandsociety.org vtsociety.org ietf.org itu.int nist.gov time.gov openmaps.orgopenstreetmap.comopenstreetmap.netopenstreetmap.orgopenstreetmaps.org osm.orgosmfoundation.orgstateofthemap.comstateofthemap.orgrolfoundation.orgrolsociety.org  smpte.org -un.org -w3.org who.int wto.org - -CTRIP c-ctrip.com colinker.com ctrip.cn ctrip.co.id ctrip.co.kr ctrip.co.th  ctrip.com ctrip.com.hk ctrip.my ctrip.sg ctripbiz.com ctripbuy.hk ctripteam.com doticloud.com easytrip.com hhtravel.comhhtravel.com.tw izuchebao.com  lvtds.com  shuntu.cn -skysea.com  suanya.cn -suanya.com -tieyou.com -toptown.cn toursbms.com trip.com tripcdn.com -vipdlt.com - -HUAWEI  dbank.com dbankcdn.com harmonyos.com hicloud.com hihonor.com honor.cn -huawei.comhuaweicloud.com  vmall.com vmallres.com - -LOGITECH logi.comlogicool.co.jp logicoolg.com logitech.biz logitech.com logitech.fr logitechg.com logitechg.frworldsfastestgamer.net - - OPENSTREETMAP openmaps.orgopenstreetmap.comopenstreetmap.netopenstreetmap.orgopenstreetmaps.org osm.orgosmfoundation.orgstateofthemap.comstateofthemap.org - -RASPBIAN raspbian.org - - XIAOMITV-ADSad.intl.xiaomi.com -ads ad.mi.com -ads ad.xiaomi.com -adsadmob.xiaomi.com -adsad1.xiaomi.com -adsmistat.xiaomi.com -adstracking.miui.com -adsadv.sec.miui.com -ads"adv.sec.intl.miui.com -ads"misc.in.duokanbox.com -ads ad.hpplay.cn -adsadeng.hpplay.cn -ads atianqi.com -ads kuyun.com -ads umeng.com -ads - - -AKAMAI ak1.net -aka-ai.com -aka-ai.net akacrypto.net akadeem.net -akadns.com akadns6.net -akaeai.com -akafms.net akahost.net -akaint.net akam.net -akamaa.com -akamah.com -akamak.com -akamam.comakamai-access.comakamai-access.netakamai-cdn.com akamai-platform-internal.netakamai-platform-staging.comakamai-platform.netakamai-regression.netakamai-staging.netakamai-sucks.netakamai-thailand.comakamai-thailand.netakamai-trials.com akamai.co.kr -akamai.com -akamai.netakamaientrypoint.netakamaietpcnctest.com#akamaietpcompromisedcnctest.com'#akamaietpcompromisedmalwaretest.comakamaietpmalwaretest.comakamaietpphishingtest.comakamaihd-staging.net akamaihd.com akamaihd.netakamaimagicmath.netakamainewzealand.comakamaiphillipines.comakamaiphillipines.netakamaitech.comakamaitech.netakamaitechnologies.comakamaitechnologies.netakamaisingapore.netakamaized-staging.net akamaized.netakamaizercentral.com -akamci.com  akami.com  akami.net -akamii.com -akamqi.com akastream.com akastream.net -akatns.net iamakamai.com iamakamai.netsoasta-dswb.com -srtcdn.net - -BITFINEX bitfinex.com -? -BOOTCSS bootcss.com -bootcdn.cnphpcomposer.com -# - MOPUB-ADS mopub.com -ads - -WIKIHOW wikihow.com -% - -ADJUST-ADS -adjust.com -ads -z -CERNcern ams02.space cern.ch cixp.netdotcernpilot.info  ippog.orglinearcollider.org - - MCDONALDSmcd  mcdonalds4008-517-517.cn -cnaboutmcdonalds.comhappymeal.co.nzhappymeal.com.au mcd.commcdelivery.co.idmcdelivery.co.krmcdelivery.com.aumcdelivery.com.mymcdelivery.com.sgmcdelivery.com.twmcdindonesia.com mcdonalds.camcdonalds.co.uk mcdonalds.commcdonalds.com.hk mcdonalds.hk mcdonalds.it mcdonalds.no mcdonalds.semcdonaldsarabia.commcdonaldsparties.co.nzmcdonaldsparties.com.au!xn--qhrx81fxh2a.xn--55qx5d.hk -B - APPLOVIN-ADS applovin.com -ads -applvn.com -ads - -MIAOPAI miaopai.com - - YOUKU-ADSactives.youku.com -ads ad.api.3g.youku.com -ads$ad.api.mobile.youku.com -ads ad.mobile.youku.com -ads(a-dxk.play.api.3g.youku.com -ads atm.youku.com -ads#b.smartvideo.youku.com -adsc.yes.youku.com -adsdas.api.youku.com -ads!das.mobile.youku.com -ads!dev-push.m.youku.com -adsdl.g.youku.com -adsdmapp.youku.com -adse.stat.ykimg.com -ads#gamex.mobile.youku.com -ads!guanggaoad.youku.com -ads hudong.pl.youku.com -ads!huodong.pl.youku.com -ads"huodong.vip.youku.com -ads hz.youku.com -adsiyes.youku.com -ads l.ykimg.com -adslstat.youku.com -ads mobilemsg.youku.com -ads msg.youku.com -adsmyes.youku.com -ads p.l.youku.com -ads pl.youku.com -ads#passport-log.youku.com -adsp-log.ykimg.com -adspush.m.youku.com -ads r.l.youku.com -ads s.p.youku.com -adssdk.m.youku.com -adsstat.youku.com -ads$statis.api.3g.youku.com -ads&store.tv.api.3g.youku.com -ads&store.xl.api.3g.youku.com -adstdrec.youku.com -adstest.ott.youku.com -ads!test.sdk.m.youku.com -ads v.l.youku.com -adsval.api.youku.com -ads wan.youku.com -adsykatr.youku.com -adsykrec.youku.com -ads - -VIDEOLAN videolan.org - -VIM vim.org - -INSIDERbusinessinsider.co.zabusinessinsider.combusinessinsider.com.aubusinessinsider.com.plbusinessinsider.debusinessinsider.esbusinessinsider.frbusinessinsider.inbusinessinsider.jpbusinessinsider.mxbusinessinsider.mybusinessinsider.nlbusinessinsider.sginsider-intelligence.com insider.cominsiderintelligence.com - -LYSOL lizol.co.in lysol.ca lysol.cllysol.cn -cn lysol.co.cr  lysol.com lysol.com.cn -cn lysol.com.mx  lysol.net -6 - NORDSTROM nordstrom.comnordstrommedia.com -C - SCIENCEDIRECTsciencedirect.comsciencedirectassets.com - -YARNPKG yarnpkg.com -1 -ABEMA abema.io abema.tv ameba.jp - - -FONTSINUSEfontsinuse.com -k -IKEAikea.cn -cn ikea.com ikea.net  ingka.com  ingka.devingkacentres.com -, -JAVAjava java.com java.net -6 -UDN udn.com -udn.com.twudnfunlife.com - -WENZHAO -wenzhao.ca - - YUANFUDAO banmaaike.comxiaoyuankousuan.com ybccode.com yuanfudao.com yuansouti.com yuantiku.com - -LETV-ADS#api.game.letvstore.com -ads ark.letv.com -ads dc.letv.com -adsdev.dc.letv.com -ads fz.letv.com -ads g3.letv.com -adsletv.allyes.com -adsminisite.letv.com -adsmsg.m.letv.com -adsn.mark.letv.com -adsplog.dc.letv.com -adspro.hoye.letv.com -ads pro.letv.com -ads stat.letv.com -ads"static.app.m.letv.com -ads -A -MEIZU flyme.cn meizu.cn  meizu.com  mzres.com - - NEURALINK neuralink.com - -SLACKslack-edge.comslack-imgs.com  slack.com -slackb.comslackcertified.com slackdemo.com slackhq.com -3 - XHAMSTER-ADS#collector.xhamster.com -ads -? -GEETEST geetest.com#geetest.datasink.sensorsdata.cn - -HEYTEA -heytea.com -B - LOCALBITCOINSlocalbitcoins.comlocalbitcoinschain.com -2 -INNER-ACTIVE-ADSinner-active.mobi -ads -F - -DEVIANTARTdeviantart.comdeviantart.net da-files.com -? -LIEPIN -liepin.com -lietou.comlietou-static.com -Z -PICSEE pics.ee  picsee.co -picsee.pro pise.pw pros.ee -pse.is - -GITV gitv.cn gitv.tv - -HUFFPOSThuffingtonpost.cahuffingtonpost.co.ukhuffingtonpost.co.zahuffingtonpost.comhuffingtonpost.com.auhuffingtonpost.com.mxhuffingtonpost.dehuffingtonpost.eshuffingtonpost.frhuffingtonpost.grhuffingtonpost.inhuffingtonpost.ithuffingtonpost.jphuffingtonpost.kr huffpost.comhuffpostarabi.comhuffpostbrasil.comhuffpostmaghreb.com - - -CLOUDFLAREcloudflare-dns.comcloudflare-ipfs.comcloudflare.comcloudflareapps.comcloudflareinsights.comcloudflareok.comcloudflaressl.comcloudflarestatus.comcloudflarestream.comcloudflaretest.com one.one.onetrycloudflare.comvideodelivery.net workers.dev - -CNPMJS -cnpmjs.org - -NUROFEN -nurofen.at -nurofen.bg -nurofen.cn -cn nurofen.co.il nurofen.co.nz nurofen.co.uk nurofen.co.za nurofen.comnurofen.com.aunurofen.com.cn -cnnurofen.com.sg -nurofen.cz -nurofen.de -nurofen.es -nurofen.fr -nurofen.hr -nurofen.hu -nurofen.ie -nurofen.it nurofen.net -nurofen.no -nurofen.pl -nurofen.pt -nurofen.ro -nurofen.sk -nurofen.runurofengel.com51nurofensk-prod-env.eu-west-1.elasticbeanstalk.com - -CATEGORY-CRYPTOCURRENCY asproex.comasproexapi.combinance.charity binance.cloud binance.combinance.vision binancezh.com bnbstatic.com yshyqxx.com -bitbank.cc bitcoin.org bitfinex.com bitflyer.com bitflyer.jp -bitmex.com bitquick.co bisq.io bisq.network bitsquare.io bitstamp.net bittrex.com btcbox.co.jp -cex.io coinbase.com coinone.co.krcoinonecore.comcoinonecorp.com dogecoin.com -devcon.orgethereum.foundation ethereum.org -hbabit.com -hbfile.net  huobi.com huobi.me  huobi.pro huobi.sc huobiasia.viphuobigroup.comhuobitoken.com -kraken.comlocalbitcoins.comlocalbitcoinschain.com okex.com -paxful.com -zb.app -zb.com zb.io zb.live - -CNN cnn.com -cnn.io cnnarabic.com cnnlabs.com cnnmoney.ch cnnmoney.comcnnmoneystream.comcnnpolitics.com - -OSDN osdn.net - -GAMERSKY gamersky.com - -91PORN -91porn.com - - ALIBABACLOUDalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.com -e - ATOM-DATA-ADS atom-data.io -adsanalytics-data.io -ads ironbeast.io -ads - -GRAVATAR gravatar.com -" - HUBBLEPHONEhubblephone.com - -TESLA solarcity.comtesla.cn -cn  tesla.com tesla.com.cn -cnteslamotors.cn -cnteslamotors.comteslamotors.com.cn -cntesla-cdn.thron.cn -cntesla-cdn.thron.com - -UPROXY -uproxy.org - -5CH 2ch.net 5ch.net -. -ANDROID android.comandroidify.com -* -BOXUN  boxun.combowenpress.com -+ - -KUBERNETES kubernetes.io -k8s.io - -NINTENDO100classicbooks.com -amiibo.comayudanintendo.combowsersinsidestory.comexcitebots.comflipnotestudio.comgloryofheracles.comhackyourconsole.comkirbysuperstarultra.comkirbysepicyarn.comlegendofzelda.cn -cn legendofzelda.com.cn -cn miitomo.commiitomo.com.cn -cn miiverse.com nentindo.netnintendo-europe-sales.comnintendo-europe.com nintendo.at nintendo.be nintendo.chnintendo.co.jpnintendo.co.krnintendo.co.uknintendo.co.za nintendo.comnintendo.com.hknintendo.com.pt nintendo.de nintendo.dk nintendo.es nintendo.eu nintendo.fi nintendo.fr nintendo.it nintendo.net nintendo.nl nintendo.no nintendo.pt nintendo.ru nintendo.se nintendo.twnintendo3ds.comnintendodsi.comnintendoeurope.comnintendolabo.cn -cnnintendonetwork.netnintendonyc.comnintendostore.comnintendoswitch.comnintendoswitch.netnintendoswitchtogether.compersonaltrainermath.complaynintendo.comsupersmashbros.cn -cn!supersmashbros.com.cn -cnthelegendarystarfy.comthewonderful101.comwariolandshakeit.comwariowarediy.comyoshisnewisland.comac-pocketcamp.comdiddykongracing.com dkr.com donkeykongcountryreturns.comfire-emblem-heroes.comfireemblemawakening.com -dialga.com giratina.com -kyurem.com pokedex3d.compokemon-moon.compokemon-sun.compokemon-sunmoon.com pokemon.compokemonbank.compokemonblackwhite.com pokemonbw.compokemonchampionships.compokemongoldsilver.compokemonhome.compokemonletsgoeevee.compokemonletsgopikachu.compokemonmysterydungeon.compokemonpicross.compokemonplatinum.compokemonrubysapphire.compokemonsunmoon.compokemonswordshield.compokemonultrasunmoon.compokemonvgc.compokemonwifi.net xn--mts47c3w9b1qr.cn -cnxn--mts47c3w9b1qr.netsplatoon2tournament.com starfox.comdrmario-world.commarioandluigidreamteam.commariobroswii.com mariokart.cn -cnmariokart.com.cn -cnmariokart7.commariokart8.commariosupersluggers.comnewsupermariobrosu.comsupermario.comsupermario3dworld.com!supermariobros.com.cn -cnsupermariogalaxy.comsupermariorun.comsuperpapermario.comnintendowii.com  wii-u.com -wiifit.com wiifitu.com wiipartyu.com wiisports.comwiisportsresort.comwiiugamepad.com  wiivc.net xenoblade.com - -GEOLOCATION-!CNacadaeafagalamanaoaqaratauawaxazbabgbhbibjbmbnbobrbsbvbwbybzcacdcgclcmcrcucwcxcyczdedkdzecegehereseufifjfkfofrgagbgdgeghgiglgmgngrgsgtgugwgyhkhnhthuieiliniqirjejmjojpkekhkiknkpkrkwkykzlblclilklrlsltlulymamcmdmfmgmhmkmnmompmqmrmtmumwmxmymznancnenfngninlnonpnrnunzompapepfpgphplpnrerorurwsascsdsesgsisjskslsnsrssstsusvsxsysztctdtfthtjtktltmtntotptrtttwtzuaugukumusuyuzvavcvevgvivnvuwfytzazmzwaflmlbnbanflnra averybbbbcgcebcrsdnbdnpdunsdvagerni globalxice locus mckinsey olayangroup -olayan praxisapseek -vistaprint vistawtc xerox athletabananarepublic  bloomberg boots calvinklein cartier chloegap gucci -hermesiwcjlc  lancaster lipsyloft mango  montblanc mrporter -netaporter -nextdirectnextnike oldnavy omega panerai  patagonia -piaget  piperlimepolo redstone -swatchaarp -abbott -abbvie aetna alcon -anthembms -boehringer caremore -cialis dabur  freseniusjnj merck mutuelle -sanofiaeg -airbus -alstom -aramco -blanco bosch -bostik -chesapeakechk crown -doosan -dupontflirfls flsmidth  gecompany grainger hitachi  honeywellifm ipirangajio komatsukoneksb  kuokgroup leclerclidl linde lottemanmtr reliance rexrothril rockwoolrwesandvikcoromant sandvik sapphiresca -schaeffler schmidt schwarzgroup schwarz senersewsncf statoil total -vanish -walter -warman weberweir woodsidezara zippo agakhanakdn -ansons -bharti -extraspace genting glean ikano -imamat  ladbrokes -livestrongmaif -mattel shell transformersvanawme astriumcern -emerck  hisamitsu lilly lundbeck lupin merckmsdmsdmtpc -otsuka -pfizer stadateva toray weather auspost deutschepostdhl epost fedex -pitneyaaa -abarthadac  alfaromeoaudi bentleybmw bridgestone bugatti buick cadillaccase -caseih  chevrolet chevy chrysler -datsun dodge -dunlop ferrarifiat  firestonefordgmc goodyear honda hyundai infiniti iveco -jaguarjeepkia lamborghini -lancia  landrover lexus lincoln maseratimini -mitsubishi mopar -newholland -nissanramseatsrt -suzuki -tatamotors -toyotatvs uconnect -volkswagen volvoaigo android apple bestbuy brother canondell digikey fujitsu  fujixerox  goldpointibm nikon nokia olympus  panasonic philips pioneer samsung sharpsony toshiba  yodobashi  allfinanzallfinanzberaterallfinanzberatungallyamericanexpressamexampanz banamex barclaycard barclaysbbtbbvabnl -bnpparibasbofa bradesco -capitalone cashbackbonuscbacfa chasecimb citadelciti commbank deloitte discover everbank fidelity hdfcbankhdfchsbcitaujcbjll jpmorganchase jpmorgankfhkpmg lacaixa lasalle lixil lplfinanciallplnab nadex netbanknorthwesternmutualopen -pictetpncpohl  pramerica -prudentialprupwcsaxosbiscbscor shriram softbank  statebank temasek -transunion ubankubs vanguardvisa wolterskluwerafamilycompany  aquarellediyduck -finish glade  homedepot  homegoods mrmuscleoffraid rightathome  scjohnsonaig allstateamericanfamily amfam amicaaxa chartis esurance farmers  goodhands guardian guardianlife iselect metlifemma -mutual -nationwide -nissaynorthlandinsurance -onyourside progressive redumbrella  statefarm -swiftcover thehartfordtiaatravelersinsurance  travelers travelguardtrvvig beats blockbusterbway netflix playstation showtimetab williamhillxboxasda bloomingdales -george  homesense justforulego macys  marshallsobiqvc safeway samsclub shopyourway staples -targetthd tiffany -tjmaxxtjx -tkmaxxvons walmartabb  accentureaceraco -active -airtel -amazonandaolawsaxis azure chatr -chrome cisco comcast -connectorscscdclkdish -docomodtvdwg emerson epson ericsson etisalat  fairwindsfido -flickr frogans frontierftrgbizgeaggeegle gmailgmogmx godaddygoog -googlegoogreeguge hangouthkt hotmailhtc -hughesieee iinet infosysinfy intel -intuitjavajcpjmpjprs juniperkddi -kindle -konamikpn lanxess liaison -lockermeo  microsoftmihmiimint mitekmit -mobily -mozaicmtn multichoice naspersnec neustar nexusnico -nortonntt -officeollo -oracle -orangeovh patchpayupccwqtel quest  richardli ricoh -rogers -sakurasaposassessfrshaw skolkovo skydrive skype sling smart stcgroupstc symantectatatcitdk telecity -telefonica terra translations  ultrabook unicornuol verisignvivavivo windows xfinity -xperia yahoo yamaxun -yandex yellowpages youtube -zappos -chanel clinique garnier  kerastase -kiehls lamer lancome -loreal -matrix -maybelline -natura origins -redkensvr barefoot beknown cityeatscookingchannel -cuisinella delmonte edekafage ferrero foodnetworkfood gallo heinz -kindermcd  mcdonalds pamperedchef -rocher -wilmarxn--3oq18vl8pn36axn--55qx5d8y0buji4b870uxn--5su34j936bgsgxn--b4w605ferd xn--cckwcxetd -xn--cg4bkixn--dkwm73cwpn xn--flw351exn--fzys8d69uvgmxn--hxt035cmppuelxn--hxt035czzpfflxn--j6w470d71isscxn--jlq480n2rgxn--jlq61u9w7bxn--kcrx77d1x4axn--kcrx7bb75ajk3bxn--mgba3a3ejtxn--mgba7c0bbn0axn--mgbaakc7dvfxn--mgbb9fbpob xn--ngbe9e0a xn--pgb3ceoj xn--qcka1pmcxn--w4r85el8fhu5dnra xn--w4rs40l avianca caravan changiairport cipriani clubmed delta firmdale -hilton hyatt kerryhotelskerrylogisiticskerryproperties marriott orientexpress  shangrila tradershotelstui -viking -virgin -webjetablecbre chintai  frontdoor -lefrakabc africamagicartebbc  canalpluscbncbsdstv -eurovisionfox -gallup globogotv guardianmediahbohgtvimdbitv -kyknetmnet movistar mzansimagicnhk nowtvsbs sevensky spiegel starhub -supersport theguardian travelchannelweatherchannel bauhaus latrobe -monashrmit pubmatic.compubmatic.co.jpads.pubmatic.com -ads taboola.com ahmia.fi akasha.worldbrowserleaks.combypasscensorship.org citizenlab.cadat.foundationdemocracy.earth eff.orgepicbrowser.com ffprofile.comfreenetproject.org fteproxy.org  gfw.press -gfw.report greatfire.orgguardianproject.infohaveibeenpwned.compwnedpasswords.com -geti2p.net ipfs.io  jitsi.orggetlantern.org -lantern.io  ntc.partyonionshare.org ooni.org openvpn.netprivacytools.io psiphon3.comshadowsocks.org softether.orgsoftether-download.com stunnel.org telex.cctorproject.org tribler.org tunsafe.comtwister.net.co -uproxy.org  v2ray.com -v2ray.cool  v2fly.org vuvuzela.io -whonix.org wikileaks.org wireguard.com -zeronet.ioartstation.comartstationmedia.com drbl.in dribbble.com dynacw.co.jp -dynacw.com dynacw.com.cn dynacw.com.hk dynacw.com.tw  fonts.com  fonts.netfontsinuse.com glyphsapp.comjiyu-kobo.co.jpfontexplorerx.com fontbook.com fontshop.comfstopimages.com40fontshop-prod-responsive-images.s3.amazonaws.com linotype.com myfonts.com myfonts.net monotype.commonotypeimaging.com bitstream.com itcfonts.commorisawa.co.jptypesquare.com -pin.it -pinimg.com pinterest.at pinterest.be pinterest.ca pinterest.ch pinterest.cl pinterest.copinterest.co.atpinterest.co.inpinterest.co.krpinterest.co.nzpinterest.co.uk pinterest.compinterest.com.aupinterest.com.bopinterest.com.ecpinterest.com.mxpinterest.com.pepinterest.com.pypinterest.com.uypinterest.com.vn pinterest.de pinterest.dk pinterest.ecpinterest.engineering pinterest.es pinterest.fr pinterest.hu pinterest.id pinterest.ie pinterest.inpinterest.info pinterest.it pinterest.jp pinterest.kr pinterest.mx pinterest.nl pinterest.nz pinterest.pe pinterest.ph pinterest.pt pinterest.ru pinterest.se pinterest.th pinterest.tw pinterest.uk pinterest.vnpinterestmail.comrealtype.co.jp realtype.jptypenetwork.comtypography.com webtype.commini4myminicard.comaboutyourmini.comatlantaminidealers.comgreatergothammini.com!midatlanticbmwmotorcycles.commini-abudhabi.commini-antilles.frmini-bahrain.commini-bosnia.com mini-clubs-international.commini-connected.atmini-connected.bemini-connected.chmini-connected.co.ukmini-connected.commini-connected.czmini-connected.demini-connected.dkmini-connected.eemini-connected.esmini-connected.fimini-connected.frmini-connected.iemini-connected.itmini-connected.ltmini-connected.lumini-connected.lvmini-connected.nlmini-connected.nomini-connected.plmini-connected.ptmini-connected.semini-corporate-sales.com mini-coupe.camini-dubai.com -mini-e.commini-egypt.commini-georgia.commini-grouparchiv.demini-grouparchive.commini-jordan.com mini-ksa.commini-kuwait.commini-lebanon.com mini-me.com mini-oman.commini-qatar.commini-srilanka.commini-stjohns.camini-stjohns.commini-tahiti.commini-vietnam.commini-windsor.camini-windsor.com mini.am mini.at mini.be mini.bg mini.by mini.ca mini.cc mini.ch mini.cl -mini.co.cr -mini.co.id -mini.co.il -mini.co.kr -mini.co.me -mini.co.th -mini.co.uk -mini.co.za mini.com mini.com.ar mini.com.bn mini.com.br mini.com.co mini.com.cy mini.com.do mini.com.ec mini.com.gr mini.com.gt mini.com.hr mini.com.mk mini.com.mo mini.com.mt mini.com.mx mini.com.pa mini.com.pe mini.com.ph mini.com.pl mini.com.py mini.com.sg mini.com.tr mini.com.tw mini.com.uy mini.cz mini.de mini.dk mini.dz mini.es mini.fi mini.fr mini.gp mini.hu mini.ie mini.in mini.is mini.it mini.jp mini.kz mini.lu mini.ma mini.md mini.mq mini.mu mini.my mini.nc mini.nl mini.no mini.pt mini.re mini.ro mini.rs mini.ru mini.se mini.si mini.sk mini.tm mini.tn mini.ua -mini101.caminiargentina.comminibrossard.caminibrossard.com minicanada.caminicaribbean.com miniccrc.ca minicooper.caminidealer.comminidealernet.comminidowntown.caminidowntown.com minidrivingexperienceusa.com minidurham.caminidurham.com  minie.com -minifs.comminigeorgian.caminigrandriver.comminihalifax.caminihalifax.com -minihk.comminiinvasion.caminiitalianjob.comminikelowna.caminikelowna.comminilangley.caminilangley.com minilat.com minilaval.ca minilaval.com minilondon.ca minilondon.cominimarkham.caminimarkham.comminimoncton.caminimoncton.comminimontrealcentre.caminimontroyal.caminimontroyal.comminimotoringrewards.comminimotoringschool.comminimotorsport.commininanaimo.camininanaimo.comminioakville.comminiofmonrovia.comminipetfriendly.comminirichmond.caminirichmond.comminisaskatoon.caminisaskatoon.com minishop.ca minispace.comminispecialoffers.caminispygear.comministcatharines.caministeagathe.comministjohns.caminitakesthestates.comminitoronto.caminitroisrivieres.ca miniusa.comminiusaservice.comminiusatires.comminivalueservice.comminivancouver.caminivaughanwest.caminivaughanwest.comminivictoria.caminivictoria.comminivilledequebec.caminivilledequebec.comminiwholesaleconnect.com miniwidget.caminiwindsor.comminiworkshop.comminiyaletown.capugetsoundmini.comshopminiusa.comsouthfloridamini.com -trmini.comtroisrivieresmini.comyours-customised.mini.combmwapprovedusedbmw.combaltimorebmw.combayareabmw.combmw-abudhabi.combmw-adventskalender.combmw-albania.combmw-antilles.frbmw-art-journey.combmw-arts-design.com bmw-asia.com bmw-auslieferungszentrum.combmw-authority-vehicles.combmw-bahrain.combmw-businessdrive.combmw-calgary.ca bmw-carit.debmw-classic.combmw-clubs-international.combmw-connected-drive.combmw-connecteddrive.aebmw-connecteddrive.atbmw-connecteddrive.bebmw-connecteddrive.bgbmw-connecteddrive.cabmw-connecteddrive.chbmw-connecteddrive.co.nzbmw-connecteddrive.co.ukbmw-connecteddrive.co.zabmw-connecteddrive.combmw-connecteddrive.com.aubmw-connecteddrive.com.brbmw-connecteddrive.com.cybmw-connecteddrive.com.hrbmw-connecteddrive.com.kwbmw-connecteddrive.com.mtbmw-connecteddrive.czbmw-connecteddrive.debmw-connecteddrive.dkbmw-connecteddrive.eebmw-connecteddrive.esbmw-connecteddrive.fibmw-connecteddrive.frbmw-connecteddrive.grbmw-connecteddrive.hubmw-connecteddrive.iebmw-connecteddrive.itbmw-connecteddrive.jpbmw-connecteddrive.krbmw-connecteddrive.ltbmw-connecteddrive.lubmw-connecteddrive.lvbmw-connecteddrive.mxbmw-connecteddrive.mybmw-connecteddrive.nlbmw-connecteddrive.nobmw-connecteddrive.plbmw-connecteddrive.ptbmw-connecteddrive.robmw-connecteddrive.rubmw-connecteddrive.sebmw-connecteddrive.sgbmw-connecteddrive.sibmw-connecteddrive.skbmw-connecteddrive.twbmw-corporate-sales.combmw-diplomatic-sales.combmw-driving-center.co.krbmw-drivingexperience.com bmw-dubai.com -bmw-eg.com bmw-fleet.net bmw-ghana.combmw-golfsport.com bmw-group.com bmw-group.netbmw-grouparchiv.debmw-grouparchives.com -bmw-gta.cabmw-i-pure-impulse.com bmw-i.jp bmw-int1.com bmw-iraq.combmw-jordan.combmw-konzernarchiv.de bmw-ksa.combmw-kuwait.com -bmw-lao.labmw-lebanon.combmw-m-safetycar.com  bmw-m.combmw-mdrivetour.com -bmw-me.combmw-military-sales.combmw-motorcycle.combmw-motorcycles.vnbmw-motorrad-abudhabi.com bmw-motorrad-authorities.combmw-motorrad-dubai.combmw-motorrad-motorsport.com!bmw-motorrad-now-or-never.com&"bmw-motorrad-service-inclusive.combmw-motorrad-test-ride.combmw-motorrad.atbmw-motorrad.bebmw-motorrad.bgbmw-motorrad.bhbmw-motorrad.bobmw-motorrad.cabmw-motorrad.chbmw-motorrad.clbmw-motorrad.cobmw-motorrad.co.idbmw-motorrad.co.krbmw-motorrad.co.nzbmw-motorrad.co.thbmw-motorrad.co.ukbmw-motorrad.co.zabmw-motorrad.combmw-motorrad.com.arbmw-motorrad.com.aubmw-motorrad.com.brbmw-motorrad.com.dobmw-motorrad.com.hrbmw-motorrad.com.mxbmw-motorrad.com.mybmw-motorrad.com.pebmw-motorrad.com.pybmw-motorrad.com.trbmw-motorrad.crbmw-motorrad.czbmw-motorrad.debmw-motorrad.dkbmw-motorrad.dzbmw-motorrad.ecbmw-motorrad.eebmw-motorrad.esbmw-motorrad.fibmw-motorrad.frbmw-motorrad.grbmw-motorrad.gtbmw-motorrad.hubmw-motorrad.inbmw-motorrad.itbmw-motorrad.jpbmw-motorrad.lubmw-motorrad.mabmw-motorrad.nlbmw-motorrad.nobmw-motorrad.pabmw-motorrad.plbmw-motorrad.ptbmw-motorrad.robmw-motorrad.rsbmw-motorrad.rubmw-motorrad.sabmw-motorrad.sebmw-motorrad.sibmw-motorrad.skbmw-motorrad.svbmw-motorrad.twbmw-motorrad.uabmw-motorrad.uybmw-motorsport.combmw-mountains.combmw-museum.combmw-museum.netbmw-nigeria.com bmw-oman.com bmw-ottawa.cabmw-pakistan.combmw-plant-munich.combmw-pma.com.sgbmw-powertrain.combmw-product-highlights.com bmw-qatar.combmw-routes.com -bmw-rp.combmw-rrdays.combmw-saudiarabia.combmw-security-vehicles.combmw-special-sales.combmw-sports.com bmw-sudan.combmw-tahiti.combmw-tunisia.combmw-vancouver.ca bmw-voli.me bmw-welt.com bmw-welt.net bmw-welt.tvbmw-werk-berlin.de bmw-world.com bmw-world.net bmw-world.tvbmw-worldfinal.combmw-yachtsport.com -bmw-yeg.ca bmw-yemen.com -bmw.am -bmw.at -bmw.az -bmw.ba -bmw.bb -bmw.be -bmw.bg -bmw.bm -bmw.bs -bmw.by -bmw.ca -bmw.cc -bmw.ch -bmw.cl  bmw.co.ao  bmw.co.cr  bmw.co.id  bmw.co.il  bmw.co.jp  bmw.co.ke  bmw.co.kr  bmw.co.nz  bmw.co.th  bmw.co.uk  bmw.co.za bmw.com -bmw.com.ar -bmw.com.au -bmw.com.bd -bmw.com.bn -bmw.com.bo -bmw.com.br -bmw.com.cn -cn -bmw.com.co -bmw.com.cy -bmw.com.do -bmw.com.ec -bmw.com.ge -bmw.com.gt -bmw.com.kh -bmw.com.ky -bmw.com.mk -bmw.com.mo -bmw.com.mt -bmw.com.mx -bmw.com.my -bmw.com.ni -bmw.com.pa -bmw.com.pe -bmw.com.ph -bmw.com.py -bmw.com.sg -bmw.com.sv -bmw.com.tr -bmw.com.tw -bmw.com.uy -bmw.com.ve -bmw.cw -bmw.cz -bmw.de -bmw.dk -bmw.dz -bmw.ee -bmw.es -bmw.fi -bmw.fr -bmw.gp -bmw.gr -bmw.hn -bmw.hr -bmw.ht -bmw.hu -bmw.ie -bmw.in -bmw.is -bmw.it -bmw.kg -bmw.kz -bmw.lc -bmw.lk -bmw.lt -bmw.lu -bmw.lv -bmw.ly -bmw.ma -bmw.md -bmw.mn -bmw.mq -bmw.mu -bmw.nc -bmw.nl -bmw.no -bmw.pl -bmw.ps -bmw.pt -bmw.re -bmw.ro -bmw.rs -bmw.se -bmw.si -bmw.sk -bmw.sn -bmw.tm -bmw.tt -bmw.ua -bmw.uz -bmw.vn bmwarchiv.at bmwarchiv.debmwartjourney.com bmwasia.combmwauslieferungszentrum.com bmwbikes.com  bmwbkk.de -bmwccrc.cabmwcenternet.combmwchampionship.combmwchampionshipusa.combmwcharitygolf.combmwcitychallenge.combmwclassic.combmwcolorado.combmwconnecteddrive.combmwcustomapparel.com bmwdcsnet.netbmwdealercareers.combmwdealerdirect.combmwdelawarevalley.com bmwfilms.combmwgroup-classic-heart.combmwgroup-classic.combmwgroup-gaad.combmwgroup-plants.combmwgroup-posdigital.combmwgroup-werke.com bmwgroup.at bmwgroup.com bmwgroup.net bmwgroup.jobsbmwgroupclassic.combmwgroupdesignworks.combmwgroupdirect.combmwgroupfs.combmwgroupinfobahn.combmwgroupna.com  bmwhk.com bmwi.ca bmwi.jpbmwiventures.combmwjamaica.com -bmwlat.combmwlifestyle.ca bmwm.combmwmagazine.combmwmagazine.de bmwmass.com bmwmass.net  bmwmc.netbmwmontreal.cabmwmotorcycles.combmwmotorcycleusa.combmwmotorrad.co.krbmwmotorrad.com.aubmwmotorrad.com.phbmwmotorrad.com.sgbmwmotorradhk.combmwmotorshowblog.combmwmotorsport.combmwmperformance.com bmwmuseum.netbmwmyanmar.combmwofannapolis.combmwofcentralpa.combmwoftulsa.combmwondemandusa.com bmworegon.combmwperformancecenter.combmwpittsburgh.combmwproductnews.com bmwsafari.com -bmwsfl.com -bmwsfl.net -bmwshop.cabmwspecialoffers.ca bmwsports.combmwstartupgarage.com bmwstep.combmwstepconnections.combmwsummerschool.combmwtampabay.com -bmwusa.combmwusatires.combmwusaservice.combmwusfactory.combmwusrideracademy.combmwvalueservice.combmwwholesaleconnect.com bmwworld.com bmwworld.net bmwworld.tvbmwzentrum.combrand-protection-team.comcencoastbmw.com chargenow.comchargenowusa.comchicagolandbmw.comconnected-drive.com daytonbmw.comdealerspeed.net desertbmw.com dreambmw.ca grupobmw.com guambmw.comhackthedrive.com hawaiibmw.com iaablog.comlasvegasbmw.com m-power.comminneapolisbmw.comminneapolisbmw.netmotorshowblog.com mybmw.ca  mybmw.comnewhampshirebmw.com  nmbmw.comocbmwdealers.comoffresspecialesbmw.ca park-now.com parkinfo.com -pdxbmw.com -phxbmw.compre-bmwgroup.jobspugetsoundbmw.com rentaride.com rentaride.derhodeislandbmw.comsdcountybmw.comshopbmwmotorcycles.comshopbmwusa.com socalbmw.comstlouisbmw.netthe-m-festival.comtristatebmw.comvancouverbmwretailers.cawashingtondcbmw.com alfera.com.hk alfera.com.my  alfera.in  alfera.myalphera-finance.co.inalphera-finance.com.hkalphera-finance.in -alphera.ca alphera.co.in alphera.co.nz alphera.comalphera.com.esalphera.com.hkalphera.com.my -alphera.de -alphera.es -alphera.ie -alphera.in -alphera.my alphera.netalpherafinance.com.hkalpherafinancialservices.esalpherafinancialservices.in alpherafs.caalpherafs.co.nzalpherafs.com.hkalpherafs.com.my alpherafs.ie alpherafs.in alpherafs.my premiumfs.de alphabet.asia alphabet.at alphabet.be alphabet.biz alphabet.chalphabet.co.hualphabet.co.ukalphabet.co.za alphabet.comalphabet.com.cn -cnalphabet.com.esalphabet.com.lvalphabet.com.mxalphabet.com.plalphabet.com.pt alphabet.cz alphabet.de alphabet.es alphabet.eu alphabet.fr alphabet.info alphabet.jp alphabet.lt alphabet.lu alphabet.lv alphabet.mxalphabet.net.cn -cn alphabet.noalphabet.org.uk alphabet.pl alphabet.pt alphabet.uk alphabet.usalphabetfinance.netelectricluxury.comrolls-roycecullinan.comrolls-roycemotorcars.comrolls-roycemotorcarsna.com  rrtis.comspiritofecstasy.com bridgestone  bandag.cc -bandag.comblizzak-juken.jpbookmybridgestonetyre.combridgestone-asiapacific.combridgestone-bandag.combridgestone-brand.com#bridgestone-business-service.jpbridgestone-korea.co.krbridgestone-plt-eng.combridgestone-tac-oman.combridgestone.clbridgestone.cn -cnbridgestone.co.crbridgestone.co.idbridgestone.co.inbridgestone.co.jpbridgestone.co.thbridgestone.combridgestone.com.arbridgestone.com.brbridgestone.com.cn -cnbridgestone.com.cobridgestone.com.mxbridgestone.com.sgbridgestone.com.twbridgestone.com.vnbridgestoneamericas.combridgestonearena.combridgestonecntc.combridgestonecomercial.clbridgestonecomercial.co.crbridgestonecomercial.com.arbridgestonecomercial.com.brbridgestonecomercial.com.cobridgestonecomercial.com.mxbridgestonegolf.combridgestonegz.combridgestonela.combridgestonemarketing.combridgestonemerchandise.com bridgestonenationalfleet.combridgestoneperformance.com#bridgestoneperformancemedia.net bridgestonerapiddelivery.combridgestonerewards.combridgestonesyb.combridgestonetire.cabridgestonetire.combridgestonetyre.com.mybridgestonevan.combridgestonewx.combs-adreview.com bs-awh.ne.jp  bsw.co.jp -bsw.jpcarcare-and-tireshop.jpdaytontrucktires.com ecoforme.jp gcrtires.comjsbridgestone.commenshin-channel.com monitrix.net mr-tireman.jpmybridgestoneeducation.com playz.jp -potenza.jpprodrive-japan.comstore-bridgestonesports.com thetread.com tiresplus.com cfna.combastropfirestone.comcrossfitfirestone.com firestone.clfirestone.co.crfirestone.com.arfirestone.com.brfirestone.com.cofirestone.com.mxfirestonebpco.comfirestonecomercial.clfirestonecomercial.co.crfirestonecomercial.com.arfirestonecomercial.com.brfirestonecomercial.com.cofirestonecomercial.com.mx!firestonecompleteautocare.comfirestonedrivestore.comfirestoneip.comfirestonerewards.comfirestonetire.cafirestonetire.comhibdontire.comwheelworks.netnokiantyres.cn -cnnokiantyres.com solarcity.comtesla.cn -cn  tesla.com tesla.com.cn -cnteslamotors.cn -cnteslamotors.comteslamotors.com.cn -cntesla-cdn.thron.cn -cntesla-cdn.thron.com volvodynafleetonline.comescandinavia-arg.comindependentoperatorcn.com road-crew.com udfs.com vfsco.at vfsco.be vfsco.bg vfsco.ca vfsco.ch vfsco.cl vfsco.co.uk vfsco.co.za  vfsco.com vfsco.com.au vfsco.com.br vfsco.com.cn -cn vfsco.com.tr vfsco.cz vfsco.de vfsco.dk vfsco.ee vfsco.es vfsco.fi vfsco.fr vfsco.hu vfsco.ie vfsco.in vfsco.it vfsco.jp vfsco.kr vfsco.lt vfsco.lv vfsco.mx vfsco.nl vfsco.no vfsco.pe vfsco.pl vfsco.ro vfsco.ru vfsco.se vfsco.sk vfsco.us vgcareers.netvolvo-truck.nl volvo.cavolvo.cn -cn  volvo.com volvo.com.cn -cn volvo.sevolvobrandshop.com volvobuses.at volvobuses.be volvobuses.ca volvobuses.ch volvobuses.covolvobuses.co.nzvolvobuses.co.ukvolvobuses.co.zavolvobuses.comvolvobuses.com.arvolvobuses.com.auvolvobuses.com.brvolvobuses.com.kwvolvobuses.com.ptvolvobuses.com.tw volvobuses.de volvobuses.dk volvobuses.es volvobuses.fi volvobuses.fr volvobuses.hk volvobuses.hu volvobuses.in volvobuses.it volvobuses.jo volvobuses.kr volvobuses.ma volvobuses.mx volvobuses.my volvobuses.no volvobuses.om volvobuses.ph volvobuses.pk volvobuses.pl volvobuses.ru volvobuses.se volvobuses.sg volvobuses.tn volvobuses.us volvoce.comvolvodefense.com volvogroup.be volvogroup.cn -cnvolvogroup.comvolvogroup.com.brvolvogroup.com.cn -cn volvogroup.de volvogroup.fr volvogroup.jp volvogroup.kr volvogroup.mx volvogroup.nl volvogroup.pe volvogroup.pl volvogroup.ru volvogroup.sevolvogrouptruckcenter.nlvolvomerchandise.comvolvopenta.comvolvopenta.com.br volvopenta.de volvopenta.es volvopenta.fr volvopenta.it volvopenta.nl volvopenta.se volvopenta.usvolvosaatio.fivolvospares.comvolvotrucks.aevolvotrucks.alvolvotrucks.amvolvotrucks.atvolvotrucks.azvolvotrucks.bavolvotrucks.bevolvotrucks.bgvolvotrucks.byvolvotrucks.chvolvotrucks.clvolvotrucks.co.aovolvotrucks.co.bwvolvotrucks.co.ilvolvotrucks.co.mzvolvotrucks.co.navolvotrucks.co.ukvolvotrucks.co.nzvolvotrucks.co.zavolvotrucks.co.zmvolvotrucks.co.zwvolvotrucks.comvolvotrucks.com.arvolvotrucks.com.auvolvotrucks.com.bnvolvotrucks.com.brvolvotrucks.com.cn -cnvolvotrucks.com.covolvotrucks.com.kwvolvotrucks.com.ptvolvotrucks.com.trvolvotrucks.com.twvolvotrucks.com.uavolvotrucks.czvolvotrucks.devolvotrucks.dkvolvotrucks.eevolvotrucks.esvolvotrucks.fivolvotrucks.frvolvotrucks.gevolvotrucks.grvolvotrucks.hkvolvotrucks.hrvolvotrucks.huvolvotrucks.idvolvotrucks.involvotrucks.itvolvotrucks.jpvolvotrucks.kgvolvotrucks.krvolvotrucks.kzvolvotrucks.ltvolvotrucks.lvvolvotrucks.mavolvotrucks.mdvolvotrucks.mkvolvotrucks.mnvolvotrucks.myvolvotrucks.netvolvotrucks.nlvolvotrucks.novolvotrucks.pevolvotrucks.phvolvotrucks.pkvolvotrucks.plvolvotrucks.psvolvotrucks.qavolvotrucks.rovolvotrucks.rsvolvotrucks.ruvolvotrucks.sevolvotrucks.sgvolvotrucks.sivolvotrucks.skvolvotrucks.tmvolvotrucks.tnvolvotrucks.uyvolvotruckcenter.bevolvotruckcenter.dkvolvotruckcenter.esvolvotruckcenter.fivolvotruckcenter.itvolvotruckcenter.krvolvotruckcenter.kzvolvotruckcenter.nlvolvotruckcenter.novolvotruckcenter.sevolvotruckrental.beudtrucksmeena.com mastercard.ae mastercard.at mastercard.az mastercard.ba mastercard.be mastercard.bg mastercard.by mastercard.ca mastercard.ch mastercard.cl mastercard.cn -cnmastercard.co.idmastercard.co.ilmastercard.co.inmastercard.co.jpmastercard.co.kemastercard.co.krmastercard.co.nzmastercard.co.thmastercard.co.ukmastercard.co.vemastercard.co.zamastercard.commastercard.com.armastercard.com.aumastercard.com.bhmastercard.com.brmastercard.com.bzmastercard.com.cn -cnmastercard.com.comastercard.com.cymastercard.com.egmastercard.com.gemastercard.com.gtmastercard.com.hkmastercard.com.kwmastercard.com.lbmastercard.com.mxmastercard.com.mymastercard.com.ngmastercard.com.pemastercard.com.phmastercard.com.samastercard.com.sgmastercard.com.trmastercard.com.twmastercard.com.vemastercard.com.vn mastercard.cz mastercard.de mastercard.dk mastercard.es mastercard.eu mastercard.fi mastercard.fr mastercard.gr mastercard.hr mastercard.hu mastercard.iemastercard.inc mastercard.it mastercard.jo mastercard.ke mastercard.kz mastercard.lu mastercard.md mastercard.my mastercard.nl mastercard.no mastercard.om mastercard.pl mastercard.pt mastercard.qa mastercard.ro mastercard.rs mastercard.ru mastercard.se mastercard.si mastercard.sk mastercard.ua mastercard.usareyoucreditwise.comborderlessprepaid.combranchportal.com idservice.incinclusivegrowthscore.com findacard.com mastercad.commastercard-email.commastercardacademy.commastercardadvisors.commastercardcenter.commastercardcenter.org*&mastercardcenterforinclusivegrowth.orgmastercardconnect.commastercardezsavings.commastercardidtheftalerts.commastercardrestaurant.commastercardservices.commastercardworldwide.commasterpassteststore.commasterintelligence.commcpeaceofmind.commyaccountglobalcash.commycardbenefits.comonedinesfree.com paypass.netpieceofplastic.compredictivetechnologies.com priceless.com pricelessmarketingengine.comsbitravelcard.comshoppercentre.com!smartcommunitiescoalition.com!smartcommunitiescoalition.org startpath.comtaptotokyo.comtasteofpremium.jptravelcontroller.comwwwpaypass.combecomeindex.comcashpassport.cacashpassport.co.zacashpassport.comcashpassport.com.brcashpassport.netcorporatecashpassport.comicashpassport.com.mxintercamcashpassport.com.mx!multicurrencycashpassport.comshinhangmc.commastercardbiz.camastercardbiz.com!mastercardbusinessnetwork.com -nab.com.aunabtravellercard.com.au 53world.com53worldsweeps.comlovethiscity.commastercardmoments.compahabicilemezsurprizler.com priceless.compriceless.com.cn -cn priceless.orgpricelessafrica.compricelessarabia.compricelessaruba.compricelessbeijing.cn -cnpricelessbeijing.com#pricelessbeijing.com.cn -cnpricelesshongkong.compricelesshonolulu.compricelesspick.compricelesssantiago.compricelesssydney.compricelesssurprises.compricelesstoronto.capricelesstv.com simplify.comsimplifycommerce.comcashpassportglobe.comgwktravelex.nl travelex.be travelex.bh travelex.catravelex.co.intravelex.co.jptravelex.co.nztravelex.co.uk travelex.comtravelex.com.autravelex.com.cn -cntravelex.com.hktravelex.com.mytravelex.com.omtravelex.com.tr travelex.de travelex.fr travelex.it travelex.qatravelexae.comtravelexch.comtravelexmoneycard.combidvestbank.co.zaworldcurrencycard.co.zanab.demdex.net&"nationalaustraliaban.tt.omtrdc.netaccount-paypal.infoaccount-paypal.netaccount-paypal.orgaccountpaypal.comaccountpaypal.netaccountpaypal.org anfutong.cn -cn anfutong.comanfutong.com.cn -cn beibao.cn -cn -beibao.com beibao.com.cn -cn bill-safe.combillmelater.combillmelater.infobillmelater.net bml.infobuyfast-paysmart.net cashify.comdevtools-paypal.comexperiencebillmelater.comfundpaypal.com -gmoney.org -i-o-u.infologin-paypal.comlogin-paypal.infomywaytopay.infomywaytopay.net -pa9pal.com paaypal.com  paily.net  paily.org -paipal.com -pavpal.com paydiant.com paylike.com -paypa1.com -paypa1.org paypaal.compaypal-activate.compaypal-activate.infopaypal-activate.orgpaypal-apac.compaypal-apps.compaypal-biz.compaypal-brandcentral.compaypal-business.compaypal-business.netpaypal-business.orgpaypal-cardcash.compaypal-cash.compaypal-center.compaypal-center.infopaypal-center.netpaypal-center.orgpaypal-communication.compaypal-communications.compaypal-communications.netpaypal-community.compaypal-community.netpaypal-comunidad.compaypal-corp.compaypal-database.compaypal-database.uspaypal-donations.compaypal-dynamic.compaypal-engineering.compaypal-europe.compaypal-excelinvoicing.compaypal-exchanges.compaypal-forward.compaypal-galactic.compaypal-gift.compaypal-gifts.compaypal-gpplus.compaypal-here.compaypal-hrsystem.compaypal-innovationlab.compaypal-integration.compaypal-japan.compaypal-knowledge.compaypal-labs.compaypal-latam.compaypal-learning.compaypal-login.compaypal-login.infopaypal-login.orgpaypal-login.uspaypal-luxury.compaypal-mainstreet.netpaypal-marketing.compaypal-media.compaypal-merchantloyalty.compaypal-mobilemoney.compaypal-network.orgpaypal-notice.compaypal-notify.compaypal-online.infopaypal-online.netpaypal-online.orgpaypal-optimizer.compaypal-pages.compaypal-photocard.compaypal-plaza.compaypal-portal.compaypal-prepagata.compaypal-prepagata.netpaypal-prepaid.compaypal-profile.compaypal-proserv.compaypal-qrshopping.orgpaypal-recargacelular.compaypal-redeem.compaypal-referral.compaypal-retail.compaypal-scoop.compaypal-search.compaypal-secure.netpaypal-secure.orgpaypal-security.netpaypal-security.orgpaypal-service.orgpaypal-signin.compaypal-signin.uspaypal-special.compaypal-specialoffers.compaypal-sptam.compaypal-support.compaypal-survey.compaypal-survey.orgpaypal-status.compaypal-team.compaypal-viewpoints.net  paypal.ca -paypal.com paypal.com.cn -cn paypal.com.hk paypal.com.sg  paypal.hk paypal.info  paypal.jp  paypal.me paypal.net.cn -cn paypal.org.cn -cn  paypal.so  paypal.uspaypalbeacon.compaypalbenefits.compaypalbrasil.compaypalcommunity.cn -cnpaypalcommunity.compaypalcommunity.netpaypalcommunity.orgpaypalcorp.compaypalcredit.compaypalcreditcard.compaypalgivingfund.org paypalhere.cn -cnpaypalhere.compaypalhere.com.cn -cnpaypalhere.infopaypalhere.netpaypalhere.org paypalhere.tv paypali.net paypalinc.compaypalindia.compaypalinsuranceservices.org paypall.compaypallabs.com paypalme.com paypalnet.net paypalnet.orgpaypalnetwork.infopaypalnetwork.netpaypalnetwork.orgpaypalobjects.compaypalonline.netpaypalonline.orgpaypalprepagata.compaypalprepagata.netpaypalservice.compaypalshopping.compaypalshopping.netpaypalsurvey.com paypalx.com paypaly.com payppal.com payypal.compdncommunity.com -pp-soc.com ppaypal.com  pppds.com pypl.com  pypl.info pypl.net pypl.tvsecure-paypal.infosecurepaypal.info sslpaypal.orgthepaypalshop.comtheshoppingexpresslane.netwww-paypal.info www-paypal.usxn--bnq297cix3a.cn -cnbraintreegateway.combraintreegateway.tvbraintreepayments.combraintreepayments.infobraintreepayments.orgbraintreepayments.tv!braintreepaymentsolutions.combraintreeps.combriantreepayments.netbriantreepayments.tv card.iogetbraintree.com krakenjs.comloanbuilder.comswiftcapital.com simility.com cashify.netswiftbank.info swiftbank.usswiftfinancial.comswiftfinancial.infoswiftfinancial.netvenmo-touch.com  venmo.com -venmo.info  venmo.net  venmo.org  cash2.comfilipino-music.net -s-xoom.comwebmoneyinfo.com2.wiremoneytoirelandwithxoomeasierandcheaper.com wwwxoom.comxoom-experience.com xoom.com xoom.io xoom.net.cn -cn xoom.us xoomcom.comvenmo.s3.amazonaws.com softbank sb-mobile.jpsb-telecom.netsoftbank-ipo.comsoftbank-jp.comsoftbank-robotics.comsoftbank-telecom.comsoftbank-telecom.net softbank.jp softbank.tvsoftbankbb.comsoftbankbb.netsoftbankci.comsoftbankhawksstore.jpsoftbankmobile.netsoftbankrobotics.comsoftbanktelecom.comsoftbankusa.netsoftbankventuresasia.comsoftbankworld.com -stripe.comstripe.network stripecdn.com visa.be visa.ca visa.clvisa.cn -cn -visa.co.ao -visa.co.cr -visa.co.id -visa.co.il -visa.co.in -visa.co.jp -visa.co.ke -visa.co.ni -visa.co.nz -visa.co.th -visa.co.uk -visa.co.ve -visa.co.za visa.com visa.com.ag visa.com.ai visa.com.ar visa.com.au visa.com.az visa.com.bo visa.com.br visa.com.bs visa.com.bz visa.com.cn -cn visa.com.co visa.com.cy visa.com.dm visa.com.do visa.com.ec visa.com.ge visa.com.gt visa.com.gy visa.com.hk visa.com.hn visa.com.hr visa.com.jm visa.com.kh visa.com.kn visa.com.kz visa.com.lc visa.com.lk visa.com.ms visa.com.mx visa.com.my visa.com.ng visa.com.pa visa.com.pe visa.com.ph visa.com.pr visa.com.py visa.com.ru visa.com.sg visa.com.sv visa.com.tr visa.com.tt visa.com.tw visa.com.ua visa.com.uy visa.com.vc visa.com.vi visa.com.vn visa.cz visa.de visa.dk visa.es visa.fi visa.fr visa.gd visa.gp visa.gr visa.hk visa.hu visa.ie visa.is visa.ky visa.lt visa.lv visa.mn visa.mq visa.nl visa.no visa.pl visa.pt visa.ro visa.se visa.sk visa.so visa.sx visa.tc -visabg.com visaeurope.at visaeurope.ch visaeurope.lu visaeurope.sivisaitalia.com visakorea.comvisamiddleeast.comvisasoutheasteurope.comfinlitsummit.orgmyvisaluxuryhotels.comvisainfinite.cavisainfiniteluxuryhotels.ca!visaluxuryhotelcollection.com$ visaluxuryhotelcollection.com.br$ visaluxuryhotelcollection.com.mxvisaluxuryhotels.comvisaluxuryhotels.com.arvisasignaturehotels.com($connaissancesfinancierespratiques.ca dnsvisa.comeubluecardvisa.comfinancialsoccer.ca"financialsoccer.com.cn -cnlightbridge.commiamifintechfestival.commhshosting.compracticalbusinessskills.compracticalmoneyskills.ca#practicalmoneyskills.cn -cn'practicalmoneyskills.com.cn -cnpracticalmoneyskills.jpsoccerfinancier.catorcidadeouro.com vaultify.com vaultify.info vaultify.net visa-atm.com visa-news.jp visabank.orgvisabusinessinsights.comvisacarddesignlab.com visacards.comvisacheckout.netvisacheckout.orgvisaconciergelac.comvisadigital.comvisadigitalconcierge.com visadns.comvisadpsonline.usvisaeverywhereshop.comvisafintechfasttrack.comvisafulfillment.comvisagiftcard.usvisaglobalfinance.comvisaicsdirect.comvisainfinite.cavisainfinite.cn -cn visanet.net -visapay.cn -cnvisapcsdirect.comvisaplatinum.cn -cnvisaplatinum.com.cn -cn visaplus.comvisasavingsedge.cavisasignature.co.kr visb.org ak1.net -aka-ai.com -aka-ai.net akacrypto.net akadeem.net -akadns.com akadns6.net -akaeai.com -akafms.net akahost.net -akaint.net akam.net -akamaa.com -akamah.com -akamak.com -akamam.comakamai-access.comakamai-access.netakamai-cdn.com akamai-platform-internal.netakamai-platform-staging.comakamai-platform.netakamai-regression.netakamai-staging.netakamai-sucks.netakamai-thailand.comakamai-thailand.netakamai-trials.com akamai.co.kr -akamai.com -akamai.netakamaientrypoint.netakamaietpcnctest.com#akamaietpcompromisedcnctest.com'#akamaietpcompromisedmalwaretest.comakamaietpmalwaretest.comakamaietpphishingtest.comakamaihd-staging.net akamaihd.com akamaihd.netakamaimagicmath.netakamainewzealand.comakamaiphillipines.comakamaiphillipines.netakamaitech.comakamaitech.netakamaitechnologies.comakamaitechnologies.netakamaisingapore.netakamaized-staging.net akamaized.netakamaizercentral.com -akamci.com  akami.com  akami.net -akamii.com -akamqi.com akastream.com akastream.net -akatns.net iamakamai.com iamakamai.netsoasta-dswb.com -srtcdn.netcloudflare-dns.comcloudflare-ipfs.comcloudflare.comcloudflareapps.comcloudflareinsights.comcloudflareok.comcloudflaressl.comcloudflarestatus.comcloudflarestream.comcloudflaretest.com one.one.onetrycloudflare.comvideodelivery.net workers.dev -fastly.com -fastly.netfastlylabs.com fastlylb.netfastly-terrarium.com -zencdn.net  imgix.com  imgix.net pagecdn.com -pagecdn.io quantil.com stackpath.com stackpath.devbuypass-ssl.com buypass.com -buypass.no -buypass.se minid.no -comodo.com comodoca.com comodoca2.com comodoca3.com comodoca4.comcybertrust.co.jpcybertrust.ne.jp digicert.com freessl.com geotrust.com rapidssl.com -thawte.com  thawte.de  thawte.fr entrust.netentrustdatacard.comglobalsign-media.com globalsign.be globalsign.chglobalsign.co.ukglobalsign.comglobalsign.com.auglobalsign.com.hkglobalsign.com.sg globalsign.es globalsign.eu globalsign.frglobalsign.net globalsign.nl!secure.globalsign.com -cnocsp.globalsign.com -cn ocsp2.globalsign.com -cn identrust.comidentrust.co.uk identrust.netletsencrypt.org secom.co.jpsecomtrust.net sectigo.comenterprisessl.comhackerguardian.cominstantssl.comoptimumssl.compositivessl.com digital-id.ch digitalid.chmanaged-pki.chmanaged-pki.de pki-post.ch pki-posta.ch pki-poste.chroot-signing.chrootsigning.comssl-certificate.ch -swissid.ch swissign.com swissign.liswisssign-group.chswisssign-group.comswisssign-group.li swisssign.ch swisssign.com swisssign.li swisssign.net swisssign.orgswisssigner.chswisssigner.comswisssigngroup.chswisssigngroup.comswissstick.com swisstsa.ch swisstsa.li trustdoc.ch trustsign.ch zert.ch telekom.com -telekom.de trustwave.com verisign.asia verisign.biz verisign.chverisign.co.inverisign.co.uk verisign.comverisign.com.auverisign.com.brverisign.com.cnverisign.com.esverisign.com.hkverisign.com.sgverisign.com.twverisign.com.vn verisign.de verisign.dk verisign.es verisign.fr verisign.hk verisign.in verisign.info verisign.jobs verisign.mobi verisign.name verisign.net verisign.org verisign.pro verisign.se verisign.sg verisign.tw verisign.us verisign.vnverisigninc.com -discord.co discord.com -discord.gg discord.gift discord.gifts discord.media discord.newdiscordapp.comdiscordapp.net freenode.net -lin.ee line.me line.naver.jp line-apps.com line-cdn.net line-scdn.net -nhncorp.jp -signal.art -signal.orgsignalusers.orgslack-edge.comslack-imgs.com  slack.com -slackb.comslackcertified.com slackdemo.com slackhq.com contest.com  graph.orgquiz.directoryt.me tdesktop.com -telega.one -telegra.ph telegram.dog telegram.me telegram.orgtelegram.space -telesco.pe -tg.dev tx.meusercontent.dev asproex.comasproexapi.combinance.charity binance.cloud binance.combinance.vision binancezh.com bnbstatic.com yshyqxx.com -bitbank.cc bitcoin.org bitfinex.com bitflyer.com bitflyer.jp -bitmex.com bitquick.co bisq.io bisq.network bitsquare.io bitstamp.net bittrex.com btcbox.co.jp -cex.io coinbase.com coinone.co.krcoinonecore.comcoinonecorp.com dogecoin.com -devcon.orgethereum.foundation ethereum.org -hbabit.com -hbfile.net  huobi.com huobi.me  huobi.pro huobi.sc huobiasia.viphuobigroup.comhuobitoken.com -kraken.comlocalbitcoins.comlocalbitcoinschain.com okex.com -paxful.com -zb.app -zb.com zb.io zb.live  adidas.at  adidas.be  adidas.ch adidas.co.in adidas.co.uk -adidas.com adidas.com.cn -cn adidas.com.sa adidas.com.tw  adidas.cz  adidas.de  adidas.dk  adidas.hu  adidas.ie  adidas.it  adidas.nl  adidas.pl  adidas.ru  adidas.se bby.combbycastatic.ca bbycloud.combbycontent.combbycontent.net bbyintl.com bbystatic.com  bbyurl.us bestbhy.combestbuy-audio.com"bestbuy-communityrelations.combestbuy-giftcard.infobestbuy-int.combestbuy-jobs.com -bestbuy.ca -bestbuy.cn -cn bestbuy.combestbuy.com.cn -cnbestbuy.com.mx bestbuy.infobestbuy24x7solutions.combestbuybusiness.com'#bestbuybusinessadvantageaccount.combestbuycanada.cabestbuycanada.combestbuycanadaltd.cabestbuycharityclassic.combestbuycoffeemakers.combestbuyethics.combestbuyforbusiness.cabestbuygsm.combestbuyideax.com!bestbuyphotoworkshoptours.combestbuyrewards.combestbuyrewardzone.ca bestbuys.combestbuysgeeksquad.combestbuysolutions.netbestbuystores.combestbuytradein.com bestbuyus.com besztbuy.com bextbuy.combienvenuechezbestbuy.ca cowboom.com futureshop.cainitproducts.commagnoliaav.com.cn -cn mybestbuy.commybestbuyclaims.commyrewardzone.com myrz.compacifickitchenandhome.comtiendabestbuy.comunbrandedproducts.comwelcometobestbuy.cawolfatbestbuy.comwolfatbestbuy.netwww-bestbuystores.comgeek-squad-support.comgeek-squad.netgeek-squad.orggeek-squads.net geeksquad.ca geeksquad.cc geeksquad.com geeksquad.net geeksquad.tvgeeksquadcares.comgeeksquadcentral.comgeeksquadforums.comgeeksquadonline.comgeeksquadprotectionplan.orggeeksquadservices.orggeeksquadwebroot.org gscanada.info -gsccdn.com  gslink.usonlinegeeksquad.comrocketfishproducts.com dealtree.orgtechliquidators.com -booking.cn -cn booking.com bstatic.comcostco-static.com -costco.comcostcobusinessdelivery.comcraigslist.org dmm.com  dmm.co.jpdmm-extension.com21centuryaccess.com4ebaytraders.com adcommerce.cn -cn adcommerce.tv appforebay.cn -cnappsonebay.net -asebay.com -baazee.com -bidbay.combidorbuyindia.com billpoint.combillpoint.info billpoint.tv billpoint.usbillpointnewzealand.com blogebay.combookclubcorner.combuiltfromebay.com buyitnow.com buyitnow.net buyitnow.org buyitnow.tvbuyitnowshop.net cafr.ca carebay.comcargigileads.com  cebay.comcollective99.comcommerceos.comconnectcommerce.cn -cn"connectcommerce.com.cn -cnconnectcommerce.hkconnectcommerce.infoconnectcommerce.tv connectedcommerce.cn -cnconnectedcommerce.comconnectedcommerce.tvcrececonebay.comcreditcardsbay.com cyber-bay.cn -cncyber-bay.com.cn -cncyber-bay.info cyber-bay.org -dba.dk dealbay.com dealtime.com  didce.com  douya.orgdreamtoplay.com  e-bay.com e-bay.it  e-bay.net eachpay.com eachpay.net  ebahy.comebay-authenticate.netebay-confirm.comebay-course.com ebay-cz.comebay-delivery.comebay-discoveries.comebay-fashion.com ebay-inc.com ebay-inc.net ebay-inc.orgebay-online.comebay-sales.comebay-stories.com ebay-us.comebay-vacation.com ebay.at ebay.be ebay.ca ebay.chebay.cn -cn -ebay.co.nz -ebay.co.uk -ebay.co.ve -ebay.co.za ebay.com ebay.com.ar ebay.com.au ebay.com.cn -cn ebay.com.ec ebay.com.hk ebay.com.mt ebay.com.my ebay.com.ph ebay.com.sg ebay.de ebay.es ebay.fr ebay.ie ebay.in ebay.it ebay.jp ebay.lt ebay.mn ebay.net.cn -cn ebay.nl ebay.org ebay.org.cn -cn ebay.ph ebay.pk ebay.pl ebay.sg ebay.us ebay.vn -ebay.yn.cn -cn -ebay.zj.cn -cn -ebay25.com -ebay68.com  ebaya.com ebayads.com ebayads.netebayadvertising.cn -cnebayadvertising.comebayanunsios.netebayauction.comebayaustralia.comebayauthenticate.com.cn ebaybags.com ebaybank.comebaybenefits.comebayboutique.com -ebayca.com -ebayca.org ebaycafe.com ebaycar.comebaycareers.com ebaycbt.co.kr ebaycdn.net ebaychina.netebayclassifiedsgroup.comebayclassifieds.cn -cnebayclassifieds.com"ebayclassifieds.com.cn -cnebayclassifieds.infoebayclassifieds.orgebayclassifieds.tvebayclassifiedsgroup.comebayclassifiedsgroup.infoebayclassifiedsgroup.orgebayclassifies.com ebayclub.com ebaycoins.com ebaycom.comebaycommercenetwork.comebaycourse.com  ebayd.com -ebayde.com ebaydesc.cn -cnebaydesc.com.cn -cnebaydlassifieds.com -ebaydns.cn -cn ebaydts.com ebayedu.comebayeletro.comebayenterprise.cn -cnebayenterprise.com!ebayenterprise.com.cn -cnebayenterprise.infoebayenterprise.netebayenterprise.tv ebayetc.comebayexpress.sgebayfashion.comebayfashion.netebayforcharity.orgebayforeclosure.orgebayfrance.comebayglobalshipping.com ebaygroup.com ebayhabit.com ebayheels.com ebayhots.com ebayimg.com ebayinc.com ebayinc.net ebayinc.org ebayincconnectedcommerce.netebayinkblog.comebayinternetsalestax.com -ebayit.comebayjewelry.com ebayjob.com -ebayla.orgebaylisting.com ebaylocal.netebaylocationsdevacances.com ebaymag.comebaymainstreet.com ebaymall.comebaymarketplace.net ebaymotors.ca ebaymotors.cn -cnebaymotors.comebaymotors.com.cn -cnebaymotors.orgebaymotorsblog.com ebaynow.com ebaynyc.com -ebayon.com -ebayon.netebayoncampus.com ebayopen.comebayopensource.comebayopensource.netebaypakistan.net ebaypark.com ebayparts.com ebaypedia.cn -cnebaypedia.com.cn -cnebayprivacycenter.com -ebayqq.com ebayradio.com ebayrtm.comebayseller.comebayshoesstore.com ebayshop.comebayshop111.comebayshopping.cn -cnebayshopping.com.cn -cnebayshopping.orgebaysocial.com ebaysocial.ru ebaysoho.com ebaysohos.com ebaystatic.cn -cnebaystatic.com ebaystore.comebaystore77.com ebaystores.cn -cn ebaystyle.comebaysweden.com  ebayt.comebaytechblog.comebaytopratedseller.netebaytrading.comebaytradingassistant.com -ebaytv.org ebayuae.netebayvakantiehuizen.com ebayvalet.comebayvietnam.netebayworlds.com  ebayy.com edisebay.com  eebay.com epinions.comeu-consumer-empowerment.comexpertmaker.comfairmarket.comfragrancebay.comfrancemail.com half.com half.com.cn -cn half.tvhalfcanada.com halfjapan.comhandbagsoutletebay.com  iebay.com irribay.comitsbetterwhenyouwinit.com liketwice.comliveauction.com milofetch.com musicbay.netmyconstructionworld.net -myebay.com  nebay.net paisapay.cc paisapay.info paisapay.tv premobay.comprivatemarketplaces.netprivatemarketplaces.us prostores.cn -cn prostores.comprostores.com.cn -cn rethink.net shopibay.net shoping.comsourcingforebay.com.cnsourcingforebay.netsourcingforebay.tv speybay.comstoresense.com -svpply.com telebay.com telesell.com texttobuy.orgtheebayshop.comtheopportunityproject.orgtowerauction.com  vendu.com weareebay.comwatch-ebay.org wwwdecide.com wwwebay.com wwwebay.net wwwwebay.com xindelu.comxn--3et96bj49ahpq.comxn--4vq475g.comxn--4vq477m.comxn--7hv594h.comxn--7hvy28f.cn -cnxn--hb4aw0g.comxn--q41am8x.comxn--qoq462m.comxn--tkry91n.comxn--ubt498knmf.comxn--xsq421m.comxn--xsq605n.comxn--xsq959n.comxn--yf1at58a.com  xxbay.com  yibei.orggittigidiyor.netgittigidiyorsikayet.com -gumtree.sg  jijiji.ca kiji.ca kijii.ca kijij.ca  kijiji.ca kijijiauto.cakijijiautos.ca kijijidns.cn -cnkijijidns.com.cn -cnkijijiforbusiness.ca -kijijii.ca kijijiraps.ca kijji.ca -kijjiji.ca automobile.fr  mobile.demobileinternational.com&mobileinternational.com.cn -cn -q10.jp qoo10.jpachievementanalytics.comcdn-terapeak.comlistinganalytics.netmarketexecutive.net tearapeak.com terapeack.com terapeak.ca terapeak.cn -cn terapeak.comterapeak.com.hk terapeak.hk terapeak.info teraperk.com terrapeak.com thebayuk.com -nexcat.com nexpart.com -nexpart.tv renchead.comwhisolutions.com wrenchead.com-)ebay-25-assets.s3-us-west-1.amazonaws.comfarfetch-apps.comfarfetch-contents.com farfetch.cn -cn farfetch.comfarfetch.com.cn -cn farfetch.netfarfetch.net.cn -cn  fflnk.net guccichimeforchange.orggucci.cn -cn  gucci.comguccimuseo.comguccitimeless.comregiongold.com -hm.com hm.com.cn -cnhomedepot-static.com homedepot.comikea.cn -cn ikea.com ikea.net  ingka.com  ingka.devingkacentres.comloveminiso.com.br miniso-au.com miniso-bh.com miniso-np.com miniso-nz.com  miniso.by  miniso.ca miniso.cn -cn  miniso.co miniso.co.id miniso.co.tz -miniso.com miniso.com.es miniso.com.gr miniso.com.mx miniso.com.pl miniso.com.py miniso.com.tr  miniso.ge  miniso.ie  miniso.jp  miniso.ma  miniso.my  miniso.pe  miniso.pk  miniso.ua  miniso.uz minisohome.cn -cnminisoindia.comminisojordan.comminisokorea.comminisolife.co.zaminisothailand.comminisovietam.vn minsio.cn -cn myminiso.com muji.com muji.com.cn -cn muji.com.hk muji.eu muji.net muji.tw muji.us mujikorea.netnikeair-nike-shoes.com airmax360.combeijingnike.combetternike.combuynikechina.comcheap-nike.comcheapnikedunks.comcheapnikeoutlet.comclassicnike.comcnnikebrand.comcustomnikeshoes.com easynike.com -eenike.comfashionnike.com finenike.comfoampositeshoes.comgoldnikeclub.com -gonike.comhandbags-nike.comhightopnikes.com -idnike.cominikesneakers.com just-do-it.cn -cn justdoit.uskicksnike1.comkidsnikeshoes.com -mynike.comnike-air-max.comnike-dunksb.com nike-fr.com nike-org.com nike-shox.cn -cn nike-uk.com nike-us.com nike-usa.com nike.cinike.cn -cn nike.com nike.com.br nike.com.hk nike.gy nike.hk  nike.shop nike.usnike.xn--hxt814e nike0594.com -nike23.comnikeadidas.comnikeairhuarache.comnikeairmax.comnikeairmaxs.comnikeairmaxshoes.comnikeairrift.com nikeasia.comnikeaustralia.com nikeaw77.com  nikeb.com nikebbn.comnikebetrue.comnikebetterworld.cn -cnnikebetterworld.com"nikebetterworld.com.cn -cnnikebetterworld.infonikebetterworld.netnikebetterworld.org nikebiz.info nikebuy.com -nikeby.com nikecdn.comnikecompany.comnikeconfluence.com nikechina.cn -cnnikechosen.com nikecloud.com nikeclub.com nikecraft.com nikedawn.com nikedunks.netnikedunksshoes.com nikeelite.comnikefactorystore.com nikefind.com nikefirm.comnikefoamposites.comnikefoampositeshoes.comnikefootballcleats.comnikefootballgloves.comnikefootballjersey.comnikefoundation.org -nikefr.comnikefrance.com nikefree.comnikefreeshoes.comnikefuelband.comnikegadgets.comnikegenealogy.com nikegolf.ca nikegolf.cn -cn nikegrid.com nikehelp.comnikehightops.comnikehyperdunk.com nikeid.cn -cn nikeid.com.cn -cnnikeidshoes.com nikeinc.comnikeinc.com.cn -cnnikeincchemistry.comnikeinstock.com -nikeit.comnikeitalia.com nikeitaly.com -nikekd.com nikekid.com -nikelab.cn -cnnikelab.com.cn -cn nikelink.comnikeliuxiang.cn -cnnikeliuxiang.comnikeliuxiang.com.cn -cn nikelives.comnikelunarglide.com nikemall.cn -cnnikemall.com.cn -cnnikemarketing.com nikenews.com -niken7.comnikeonlinestore.comnikeoutletstore.comnikeoutletstores.comnikepioneer.com nikeplus.com nikeplus.org nikepress.com nikeprice.comnikeproduct.comnikepromax.comnikeresponsibility.comnikereuseashoe.comnikerunclub.cn -cnnikerunclub.com.cn -cnnikerunner.comnikerunning.com.cn -cnnikerunningshoes.com -nikesb.comnikesbdunks.netnikeseason.comnikeselling.comnikesellorder.comnikeshoemarket.comnikeshoes-store.comnikeshoes21.comnikeshoes4u.comnikeshoescity.comnikeshoesgroup.comnikeshoesinc.comnikeshoesmarket.comnikeshoesretro.comnikeshoeswon.comnikeshoponline.comnikeshoxsale.com nikesku.comnikesnowboarding.comnikesoccercleats.comnikesportswear.comnikestares.com nikestore.cn -cn nikestore.comnikestore.com.aunikestyles.com nikeswim.comniketaiwan.netniketracking.comniketradeweb.comniketrainer.comniketrainers.comniketraining.com nikeusa.com nikewear.com  nikey.com nikezoom.com nke6.comoutletnike.com runnike.comrunningnike.comsale-nikeshoes.comstatic-nike.com swoo.sh -swoosh.com  swoosh.tvthedailysnkr.comverygoodnike.comwomensnikeshox.comxn--74q035i.xn--hxt814e nordstrom.comnordstrommedia.com shopee.cn -cn shopee.co.id shopee.co.th -shopee.com shopee.com.br shopee.com.my  shopee.ph  shopee.sg  shopee.tw  shopee.vnshopeemobile.com shopeesz.com -cn -shp.ee -target -target.comtargetimg1.com walmart wal-mart.com -wal.cowalmart-content.com walmart.comwalmart.pharmacywalmartimages.com -wmt.cobetterexplained.com brilliant.org apa.orgpsyccareers.com cambridge.org"cambridgeschoolshakespeare.comcambridgedigital.netcambridgemaths.orgnewisiknowledge.comisiknowledge.com clarivate.comwebofknowledge.comwebofscience.com doi.org doi.info els-cdn.comelsevier-ae.com elsevier.com elsevier.ioengineeringvillage.com  evise.com -scopus.comscholar.google.aescholar.google.atscholar.google.bescholar.google.bgscholar.google.cascholar.google.catscholar.google.chscholar.google.clscholar.google.cnscholar.google.co.crscholar.google.co.idscholar.google.co.ilscholar.google.co.inscholar.google.co.jpscholar.google.co.krscholar.google.co.nzscholar.google.co.thscholar.google.co.ukscholar.google.co.vescholar.google.co.zascholar.google.comscholar.google.com.arscholar.google.com.auscholar.google.com.boscholar.google.com.brscholar.google.com.coscholar.google.com.cuscholar.google.com.doscholar.google.com.ecscholar.google.com.egscholar.google.com.gtscholar.google.com.hkscholar.google.com.lyscholar.google.com.mxscholar.google.com.myscholar.google.com.nischolar.google.com.pascholar.google.com.pescholar.google.com.phscholar.google.com.pkscholar.google.com.prscholar.google.com.pyscholar.google.com.sgscholar.google.com.svscholar.google.com.trscholar.google.com.twscholar.google.com.uascholar.google.com.uyscholar.google.com.vnscholar.google.czscholar.google.descholar.google.dkscholar.google.esscholar.google.fischolar.google.frscholar.google.grscholar.google.hnscholar.google.hrscholar.google.huscholar.google.isscholar.google.itscholar.google.lischolar.google.ltscholar.google.luscholar.google.lvscholar.google.nlscholar.google.noscholar.google.plscholar.google.ptscholar.google.roscholar.google.ruscholar.google.sescholar.google.sischolar.google.sk ic.ac.ukimperial.ac.ukimperialbusiness.schoolimperial.insendi.commit mit.edu mit.netmitpressjournals.orgamamanualofstyle.com anb.org blackstonespoliceservice.com epigeum.com oed.com  omscr.com oup.com -ouplaw.com ox.ac.ukoxfordaasc.comoxfordartonline.comoxfordbibliographies.comoxfordclinicalpsych.com oxforddnb.comoxfordfirstsource.comoxfordhandbooks.comoxfordlawtrove.comoxfordmedicine.comoxfordmusiconline.comoxfordpoliticstrove.com oxfordre.comoxfordreference.comoxfordscholarlyeditions.comoxfordscholarship.comoxfordwesternmusic.comsipriyearbook.org ukwhoswho.com"universitypressscholarship.comveryshortintroductions.comserialssolutions.com proquest.comsciencedirect.comsciencedirectassets.com -apress.com springer.comspringerlink.comspringernature.com wkap.nl tandf.co.uktandfonline.comtaylorandfrancis.comtaylorfrancis.com -aclweb.org acm.org altmetric.comanalytictech.comannualreviews.org  arxiv.org asha.org berkeley.edubiomedcentral.com biorxiv.org  brill.com cell.comcomputingreviews.com degruyter.comdentalhypotheses.comelifesciences.org europepmc.orgfrontiersin.org gale.com galegroup.com  ggsrv.com hindawi.com informs.orgisca-speech.orgjamanetwork.comjapanknowledge.comjbe-platform.com jmlr.org jneurosci.org  jstor.org kuke.comlawdata.com.twliteratumonline.com mdpi.com -nature.com nejm.org neurology.org ovid.com  peerj.comphysiology.org plos.org pnas.org princeton.eduresearchgate.netroutledgehandbooks.comroyalsocietypublishing.org sagepub.comscholarpedia.orgsciencemag.orgscientificamerican.com scitation.orgsilverchair-cdn.comspiedigitallibrary.orgstatsmakemecry.com thelancet.com uchicago.edu ucla.edu  umass.edu  wiley.com yale.educern ams02.space cern.ch cixp.netdotcernpilot.info  ippog.orglinearcollider.orgchinapower.csis.org code.orgcodecademy.comcoursera-for-business.orgcoursera.community coursera.help coursera.org edx.org edx-cdn.org -egghead.iofreecodecamp.orgkhanacademy.org kastatic.org -knovel.com laracasts.commasterclass.com mzed.com oreil.ly oreilly.comoreilly.com.cn -cnoreilly.revieworeillystatic.com -orl.ly sitepoint.comskillshare.com ted.com -tedcdn.com udacity.com  udemy.com udemycdn.com w3schools.com abema.io abema.tv ameba.jp ao3.orgarchiveofourown.orgarchiveofourown.comarchiveofourown.net -att.tv atttvnow.comattwatchtv.com directv.com  dtvce.comnettyinternet.com gamer.com.twbahamut.com.twgamer-cds.cdn.hinet.netgamer2-cds.cdn.hinet.net bandcamp.com -bcbits.com bethesda.net doom.comelderscrolls.com blizzard.cn -cn blizzard.comblizzardgearstore.comblz-contentstack.com blzstatic.cn -cn -battle.netbattlenet.com.cn -cn bnet.163.com -cnblizzard.nefficient.co.krblzddist1-a.akamaihd.netblzddistkr1-a.akamaihd.netbnetcmsus-a.akamaihd.netbnetproduct-a.akamaihd.netbnetshopus.akamaized.net diablo3.comdiabloimmortal.comdi.res.netease.com -cn!diablo3.nosdn.127.net -cnfiresidegatherings.complayhearthstone.com%hearthstone.nosdn.127.net -cnheroesofthestorm.com%hearthstone.nosdn.127.net -cn"heroes.nos.netease.com -cnplayoverwatch.com#overwatch.nosdn.127.net -cn starcraft.comstarcraft2.comsc2.nosdn.127.net -cnplaywarcraft3.comworldofwarcraft.com wowchina.com -cnwow.nosdn.127.net -cnblizzcon-a.akamaihd.netblz.nosdn.127.net -cnblzmedia-a.akamaihd.netblznav.akamaized.net blubrry.com  castro.fmdeviantart.comdeviantart.net da-files.comabc-studios.com abc.com edgedatg.com espn.com espncdn.com watchespn.comespn.hb.omtrdc.netespndotcom.tt.omtrdc.net -marvel.commarvel10thanniversary.commarveldimensionofheroes.commarvelparty.netmarvelpinball.commarvelsdoubleagent.commarvelspotlightplays.com marvelsuperheroseptember.commarvelsuperwar.comthemarvelexperiencetour.comnatgeomaps.comnationalgeographic.com"nationalgeographicpartners.com ngeo.comadventuresbydisney.com -babble.com -dilcdn.comdisney-asia.comdisney-discount.comdisney-plus.netdisney-studio.comdisney-studio.net  disney.ca -disney.com disney.com.br  disney.iodisneyadsales.comdisneyarena.comdisneyaulani.comdisneybaby.comdisneycareers.comdisneychannelonstage.comdisneychannelroadtrip.comdisneycruisebrasil.comdisneyenconcert.comdisneyiejobs.comdisneyinflight.comdisneyjunior.comdisneyjuniortreataday.comdisneylatino.comdisneymeetingsandevents.comdisneymovieinsiders.comdisneymusicpromotion.comdisneynewseries.com disneynow.comdisneypeoplesurveys.comdisneyplus.comdisneyredirects.comdisneysrivieraresort.comdisneystore.comdisneysubscription.comdisneytvajobs.comdisneyworld-go.com -dssott.comgo-disneyworldgo.com -go.com  mickey.tvmoviesanywhere.complaymation.comshopdisney.comshops-disney.comsorcerersarena.comspaindisney.com starwars.com#starwarsgalacticstarcruiser.comstreamingdisney.netthestationbymaker.comthisispolaris.comwatchdisneyfe.com dawngate.com eastore.comlordofultima.com -origin.com  origin.tv#cloudsync-prod.s3.amazonaws.comorigin-a.akamaihd.netrtm.tnt-ea.comssl-lvlt.cdn.ea.comawayoutgame.combattlefieldbadcompany2.combattlefront2.combattlefrontii.combejewled-stars.combejeweledstars.comcapitalgames.com chillingo.comcommandandconquer.comconquerwithcharacter.com  crysis.jp dawngate.comdawngatechronicles.com -dicela.com -dicela.netea-anz-press.com -ea.com eaaccess.comeacashcard.com eacodigos.com eakorea.co.kr eamobile.com eanordic.com -eaplay.com easports.com easports.jpeasportsactive.comeasportsactiveonline.comeasportsfootball.comeasportsfootballclub.comeasportsmma.comeasportsworld.comelectronicarts.comelectronicarts.fr futpromos.com futunited.comghostgames.comheroesofdragonage.com hutpromos.commaddenchampionship.commaddenrewards.commaddenseason.infomaddenseason.netmaddenseason.org  maxis.commysimsracing.com play4free.com playapex.com pogo.com pogobeta.com -popcap.comprojectapex.com pvzheroes.com spearhead.krspeedhunters.com  spore.com ssx3.comstarwarsbattlefront.comstarwarsbattlefront2.comtiberiumalliances.com tiburon.com -tnt-ea.comultimaforever.com unravel2.comunraveltwo.comvisceralgames.com -yogify.comanthemgame.comanthemthegame.comapexlegends.comprojectapex.combattlefield.combattlefield1943.combattlefield3.combattlefield4.combattlefield5.combattlefieldheroes.combattlefieldv.com battlelog.com bioware.combiowarestore.com eamythic.com eamythic.net cncrivals.comcommandandconquer.comtiberiumalliances.comcamelot-europe.comcamelotherald.comcamelotherald.net daoc.netdarkageofcamelot.comdarkness-risen.comdeadspacegame.com dice.se dragonage.comdragonagekeep.comdragonagemovie.comthedreadwolfrises.comdungeonkeeper.cndungeonkeeper.comdungeonkeeper.com.cneafootballworld.comfifastreet.comfifastreet3.com frostbite.comindustrialtoys.commasseffect.commasseffectarchives.comeamirrorsedge.commirrorsedge.commirrorsedge.jpmirrorsedge.netmirrorsedge2.commirrorsedge2d.comblackboxgames.comeablackbox.comneedforspeed.comneedforspeedboost.comneedforspeeddriftkings.comneedforspeedeliminator.comneedforspeedlightning.comneedforspeedoverdrive.comneedforspeedproven.comneedforspeedredline.comneedforspeedshowdown.comneedforspeedstreetkings.comneedforspeedtakedown.comneedforspeedtherun.comneedforspeedtimeattack.com#needforspeedundergroundeast.com nfsworld.comteamneedforspeed.complantsvszombies2.com -pvzgw2.com pvzheroes.comseaofsolitude.comsimcity-buildit.com simcity.com -skate2.com spearhead.krstarwarsfallenorder.comstarwarsjedifallenorder.comstarwarstheoldrepublic.comswjedifallenorder.com  swjfo.com  swtor.com  swtor.net -mysims.com thesims.com thesims3.com thesims4.comthesimssocial.com titanfall.commythicentertainment.commythicentertainment.netmythicgames.comultimaonline.com -uo.com uoherald.com xdsummit.comea.tt.omtrdc.neteaassets-a.akamaihd.netbattlebreakers.com epicgames.com fortnite.com paragon.complayparagon.comroborecall.comshadowcomplex.com spyjinx.comunrealengine.comunrealtournament.com6*^cdn\d-epicgames-\d+\.file\.myqcloud\.com$ -cn*&^epicgames-download\d\.akamaized\.net$;/^epicgames-download\d-\d+\.file\.myqcloud\.com$ -cn fireside.fm gog.comgog-statics.comgog.qtlglb.com -cn$gogalaxy.gog-statics.com -cn'menu-static.gog-statics.com -cn'productcard.gog-statics.com -cn(static-login.gog-statics.com -cn'www4-static.gog-statics.com -cn, ^images(-\d)?\.gog-statics\.com$ -cn cinemax.comforthethrone.com hbo.com hboasia.com  hbogo.com hbogoasia.com hbogoasia.hk -hbomax.com -hbonow.com  maxgo.com -112263.com callhulu.comfindyourlimits.com freehulu.com  hooloo.tv  hoolu.com hoolu.tv hu1u.com huloo.cc huloo.tv hulu.com hulu.tv hulu.ushuluaction.com -huluad.com huluapp.com huluasks.com hulucall.com hulufree.com hulugans.comhulugermany.com -hulugo.com -huluim.comhuluinstantmessenger.com huluitaly.com hulunet.comhulunetwork.com huluplus.comhulupremium.comhulupurchase.com -huluqa.comhulurussia.com huluspain.comhulusports.comhulustream.com huluteam.com -hulutv.com huluusa.comjoinmaidez.com mushymush.tv -myhulu.comoriginalhulu.com payhulu.comregisterhulu.comthehulubraintrust.comwwwhuluplus.com imdb.com imdb.tomedia-imdb.cominternationalsaimoe.comkingkong.com.twkingkongapp.com litv.tv megaphone.fm moov.hk fast.com -netflix.ca netflix.com netflix.netnetflixinvestor.comnetflixtechblog.com nflxext.com nflximg.com nflximg.netnflxsearch.net -nflxso.net nflxvideo.netnico -nicodic.jp nicomanga.jp niconico.com nicoseiga.jp nicovideo.jp nimg.jp simg.jp100classicbooks.com -amiibo.comayudanintendo.combowsersinsidestory.comexcitebots.comflipnotestudio.comgloryofheracles.comhackyourconsole.comkirbysuperstarultra.comkirbysepicyarn.comlegendofzelda.cn -cn legendofzelda.com.cn -cn miitomo.commiitomo.com.cn -cn miiverse.com nentindo.netnintendo-europe-sales.comnintendo-europe.com nintendo.at nintendo.be nintendo.chnintendo.co.jpnintendo.co.krnintendo.co.uknintendo.co.za nintendo.comnintendo.com.hknintendo.com.pt nintendo.de nintendo.dk nintendo.es nintendo.eu nintendo.fi nintendo.fr nintendo.it nintendo.net nintendo.nl nintendo.no nintendo.pt nintendo.ru nintendo.se nintendo.twnintendo3ds.comnintendodsi.comnintendoeurope.comnintendolabo.cn -cnnintendonetwork.netnintendonyc.comnintendostore.comnintendoswitch.comnintendoswitch.netnintendoswitchtogether.compersonaltrainermath.complaynintendo.comsupersmashbros.cn -cn!supersmashbros.com.cn -cnthelegendarystarfy.comthewonderful101.comwariolandshakeit.comwariowarediy.comyoshisnewisland.comac-pocketcamp.comdiddykongracing.com dkr.com donkeykongcountryreturns.comfire-emblem-heroes.comfireemblemawakening.com -dialga.com giratina.com -kyurem.com pokedex3d.compokemon-moon.compokemon-sun.compokemon-sunmoon.com pokemon.compokemonbank.compokemonblackwhite.com pokemonbw.compokemonchampionships.compokemongoldsilver.compokemonhome.compokemonletsgoeevee.compokemonletsgopikachu.compokemonmysterydungeon.compokemonpicross.compokemonplatinum.compokemonrubysapphire.compokemonsunmoon.compokemonswordshield.compokemonultrasunmoon.compokemonvgc.compokemonwifi.net xn--mts47c3w9b1qr.cn -cnxn--mts47c3w9b1qr.netsplatoon2tournament.com starfox.comdrmario-world.commarioandluigidreamteam.commariobroswii.com mariokart.cn -cnmariokart.com.cn -cnmariokart7.commariokart8.commariosupersluggers.comnewsupermariobrosu.comsupermario.comsupermario3dworld.com!supermariobros.com.cn -cnsupermariogalaxy.comsupermariorun.comsuperpapermario.comnintendowii.com  wii-u.com -wiifit.com wiifitu.com wiipartyu.com wiisports.comwiisportsresort.comwiiugamepad.com  wiivc.net xenoblade.com op.ggopgg-static.akamaized.net overcast.fm  pixiv.net  pixiv.org pixiv.co.jp  pximg.net playstationplaystation.complaystation.net sonyentertainmentnetwork.com -pca.stpocketcasts.comhistoryofdota.comhistoryofdota.nethistoryofdota.orginstituteofwar.orgmolesports.com rgpub.ioriot-games.com riot.com riot.net riotcdn.netriotgames.co.kr riotgames.comriotgames.info riotgames.jp riotgames.net riotgames.tv riotpin.comriotpoints.com rstatic.net supremacy.com supremacy.net championshipseriesleague.com lcsmerch.comleaguehighschool.comleagueoflegends.caleagueoflegends.cn -cnleagueoflegends.co.krleagueoflegends.comleagueoflegends.infoleagueoflegends.krleagueoflegends.netleagueoflegends.orgleagueoflegendsscripts.comleaguesharp.infoleaguoflegends.comlearnwithleague.comlol-europe.com lolclub.org lolespor.comlolesports.com lolfanart.net -lolpcs.com lolshop.co.kr lolstatic.comlolusercontent.com -lpl.com.cn -cn pvp.net -pvp.tv ulol.comlolstatic-a.akamaihd.netrockstargames.com -rsg.sc1-gamedownloads-rockstargames-com.akamaized.net)%media-rockstargames-com.akamaized.net*&videos-rockstargames-com.akamaized.net fandango.com mgo.commgo-images.com ravm.tv roku.com rokutime.com dishworld.comslinginternational.com  sling.commovenetworks.com -movetv.comimageworks.commotionpictureser.com playshow.ioplayshowtv.comsonyclassics.comsonypictures.comsonypicturesanimation.comsonypicturesmuseum.comsonypicturesstudios.comsonypicturestelevision.com#sonypicturestelevisiongames.com wheelpop.com -wwtbam.comyourfantasybeginsnow.comsoundcloud.com -sndcdn.com byspotify.com pscdn.co scdn.co spoti.fispotify-everywhere.com spotify.comspotify.designspotifycdn.comspotifycharts.comspotifycodes.comspotifyforbrands.comspotifyjobs.com fanatical.comhumblebundle.complayartifact.com -s.teamsteam-chat.comsteamcommunity.comsteamcontent.comsteamgames.comsteampowered.comsteamstatic.comsteamusercontent.comunderlords.comvalvesoftware.com wmsjsteam.com -cn csgo.wmsj.cn -cndl.steam.ksyna.com -cn dota2.wmsj.cn -cn st.dl.bscstorage.net -cnst.dl.eccdnx.com -cn!st.dl.pinyuncloud.com -cn&steampowered.com.8686c.com -cn%steamstatic.com.8686c.com -cn steambroadcast.akamaized.netsteamcdn-a.akamaihd.net!steamcommunity-a.akamaihd.netsteamstore-a.akamaihd.net#steamusercontent-a.akamaihd.net"steamuserimages-a.akamaihd.netsteamunlocked.net syosetu.com -muscdn.com -musical.ly -tiktok.com tiktokcdn.com tiktokv.com#p16-tiktokcdn-com.akamaized.net ext-twitch.tv  jtvnw.net  ttvnw.net  twitch.tv twitchcdn.net twitchsvc.net ubi.com ubisoft.com  uplay.com,(ubisoft-uplay-savegames.s3.amazonaws.com,(ubisoft-orbit-savegames.s3.amazonaws.com)uplaypc-s-ubisoft.cdn.ubi.com -cn -vhx.tv -vhxqa1.com -vhxqa2.com -vhxqa3.com -vhxqa4.com -vhxqa6.comvimeo-staging.comvimeo-staging2.com  vimeo.com vimeo.frvimeobusiness.com vimeocdn.comvimeogoods.comvimeoondemand.comvimeostatus.comlivestream.com abow.jp angelbeats.jp -anigema.jpcharlotte-anime.jpclannad-movie.jpheaven-burns-red.comkamisama-day.jpkamisama-maeda-lab.com koubaibu.jplitbus-anime.com product.co.jprewrite-anime.tvvisualarts.gr.jpvisual-arts.jp -viu.tvapi.viu.now.com^ewcdn[0-9]{2}\.nowe\.com$ viu.com dfp6rglgjqszk.cloudfront.net!d1k2us671qcoau.cloudfront.net!d2anahhhmp1ffz.cloudfront.net -weverse.io wwe.com wwe9.comcallersbane.com minecraft.netminecraftshop.com -mojang.comforzamotorsport.netforzaracingchampionship.com forzarc.comorithegame.comrenovacionxboxlive.comtellmewhygame.com xbox.co xbox.com xbox.eu xbox.org -xbox360.co xbox360.com -xbox360.eu xbox360.org -xboxab.comxboxgamepass.comxboxgamestudios.com xboxlive.cn -cn xboxlive.com -xboxone.co xboxone.com -xboxone.euxboxplayanywhere.comxboxstudios.com yande.re -got001.com -got002.com jstucdn.com playcvn.com rrys.tv rrys2019.com rrysapp.com  yyets.com zmz2019.com -zmzapi.com cvnad.com -ads -iyoowi.com -ads -0x0.st db.ttdropbox-dns.com dropbox.com dropbox.techdropboxapi.comdropboxbusiness.comdropboxcaptcha.comdropboxforum.comdropboxforums.comdropboxinsiders.comdropboxmail.comdropboxpartners.comdropboxstatic.comdropboxusercontent.comgetdropbox.com&"paper-attachments.s3.amazonaws.com -gfycat.com  imgur.com imgurinc.com embed-cdn.comstreamable.com 4cdn.org  4chan.org 4channel.org 2ch.net 5ch.net avsforum.com dcard.io dcard.tw  doom9.org -lih.kg  lihkg.com pincong.rocks  quora.com quoracdn.net qr.ae -reddit.comredditstatic.comredditmail.comredditmedia.com redd.it 9cdn.net9nation.com.au 9news.com.au 9now.com.aufreeviewplus.net.au -mi9.com.au -mi9cdn.com nine.com.aunineentertainment.com.aunineentertainmentco.com.auninemediaroom.com.auninemsn.com.austatic9.net.au yourtv.com.au9to5google.com 9to5mac.com9to5terminal.com 9to5toys.com dronedj.com electrek.cospaceexplored.com afp.com afpforum.com -ajplus.net aljazeera.com aljazeera.netpalestineremix.comsarajevopodopsadom.comsrebrenica360.com syhacked.comamericasvoice.news -ap.org -apnews.comappledaily.comappledaily.com.hkappledaily.com.tw appledaily.hkapplefruity.comapplehealth.com.hk -atnext.combestmallawards.com deluxe.com.hk eracom.com.tw next.hknextdigital.com.hknextdigital.com.twnextfilm.com.hknextmag.com.tw nextmedia.comnextmedia.com.tw nextmgz.comnextplus.com.hk nexttv.com.twnextwork.com.hknextwork.com.tw nextwork.hk nextwork.tw nxtdig.com.hk nxtdig.com.tw omoplanet.com privilege.hk privilege.tw sharpdaily.tw tomonews.nettwnextdigital.comappleinsider.com  asahi.com asahicom.jpasahishimbun.sc.omtrdc.netbbc  bbc.co.uk bbc.com -bbci.co.uk bbgwatch.com  bloomberg bbgevent.com bbhub.io -bbthat.com bgov.com bloomberg.cnbloomberg.co.jpbloomberg.co.kr bloomberg.combloomberg.com.brbloombergapps.combloombergarcade.combloombergbeta.combloombergbna.combloombergbreakaway.combloombergbriefs.combloombergchina.combloombergenvironment.combloombergforeducation.combloomberggovernment.combloombergindices.combloombergindustry.combloomberglabs.combloomberglaw.combloomberglive.combloomberglp.combloombergmedia.combloombergnext.combloombergpolarlake.combloombergprep.combloombergquint.combloombergradio.combloombergsef.combloombergspace.combloombergstatus.combloombergsurvey.combloombergtax.combloombergtax1.combloombergtaxtech.com!bloombergtradingchallenge.combloombergtv.mnbloombergvault.combloombergview.comblpcareers.com blpevents.comblpprofessional.com bmia.org bnef.combusinessweekmag.comlondonmithraeum.comneweconomyforum.comnewenergyfinance.comtechatbloomberg.com  boxun.combowenpress.com breitbart.comcabletv.com.hk i-cable.comcaijinglengyan.comcbs showtime cbs.comcbscorporation.com cbsi.com -cbsi.video -cbsiam.com  cbsig.net -cbsimg.netcbsinteractive.comcbsistatic.com cbsivideo.com cbsnews.comcbspressexpress.com cbssports.com cbsstatic.com cbssvideo.com -viacbs.com -viacom.com viacomcbs.comviacomcbspressexpress.com paramount.comchinadecoding.comchinadigitaltimes.net cnbc.com -cnbcfm.com -nbcuni.comnbcudigitaladops.com cnn.com -cnn.io cnnarabic.com cnnlabs.com cnnmoney.ch cnnmoney.comcnnmoneystream.comcnnpolitics.com -cup.com.hk d100.net  anm.co.uk  dailym.aidailymail.co.uk dailymail.comdailymail.com.au dailymail.dkdailymailonline.comdmgmediaprivacy.co.ukmailhealth.commailonline.co.ukmailonline.commailonsunday.co.ukmailonsunday.ie metro.co.uk -mol.imthisismoney.co.uk -thismon.ee -dw.com dajiyuan.com dajiyuan.euepochmediagroup.comepochstories.com epochtime.comepochtimes-romania.comepochtimes.co.ilepochtimes.co.krepochtimes.co.ukepochtimes.comepochtimes.com.auepochtimes.com.brepochtimes.com.sgepochtimes.com.twepochtimes.com.ua epochtimes.cz epochtimes.de epochtimes.eu epochtimes.fr epochtimes.it epochtimes.jp epochtimes.nl epochtimes.pl epochtimes.ru epochtimes.seepochtimestr.com epochweek.comepochweekly.com erabaru.netlagranepoca.com ntd.com ntdtv.ca ntdtv.co.kr  ntdtv.com ntdtv.com.tw ntdtv.jp ntdtv.kr  ntdtv.org ntdtv.ru ntdtvla.com  ntdvn.comtheepochtimes.comtheepochtimessubscribe.comvelkaepocha.cz -forbes.com forbesimg.comfoxfox-corporation.com fox-news.com fox.com -fox.tv fox10.tv fox10news.comfox10phoenix.com  fox11.comfox13memphis.com fox13news.com  fox23.comfox23maine.com -fox247.com  fox247.tv  fox26.comfox26houston.comfox28media.com  fox29.comfox2detroit.com fox2news.com  fox32.comfox32chicago.comfox35orlando.comfox38corpuschristi.com fox42kptm.com  fox46.comfox46charlotte.com  fox47.com fox49.tv fox4news.com fox51tns.netfox5atlanta.com -fox5dc.com -fox5ny.com fox5storm.com fox6now.com fox7.comfox7austin.com fox9.comfoxacrossamerica.comfoxaffiliateportal.comfoxandfriends.com -foxbet.comfoxbusiness.comfoxbusiness.tvfoxbusinessgo.comfoxcanvasroom.comfoxcareers.comfoxcharlotte.com foxcincy.com foxcincy.jobs foxcincy.netfoxcollegesports.comfoxcorporation.comfoxcreativeuniversity.com foxcredit.com foxcredit.org foxd.tv -foxdcg.comfoxdeportes.comfoxdeportes.netfoxdeportes.tvfoxdigitalmovies.com foxdoua.comfoxentertainment.com -foxest.com -foxfaq.com -foxfdm.com foxfiles.com -foxinc.com foxkansas.com  foxla.com foxla.tvfoxlexington.comfoxmediacloud.com foxnation.comfoxnebraska.com -foxneo.comfoxneodigital.comfoxnetworks.infofoxnetworksinfo.com -foxnews.cc foxnews.com foxnews.net foxnews.org -foxnews.tvfoxnewsaffiliates.comfoxnewsaroundtheworld.comfoxnewsb2b.comfoxnewschannel.com foxnewsgo.net foxnewsgo.org foxnewsgo.tvfoxnewshealth.comfoxnewslatino.comfoxnewsmagazine.comfoxnewsnetwork.comfoxnewsopinion.comfoxnewspodcasts.comfoxnewspolitics.comfoxnewsradio.comfoxnewsrundown.comfoxnewssunday.com  foxon.comfoxphiladelphia.com foxpoker.com  foxrad.io foxredeem.comfoxrelease.comfoxrichmond.com foxrobots.comfoxsmallbusinesscenter.comfoxsmallbusinesscenter.netfoxsmallbusinesscenter.org foxsoccer.net foxsoccer.tvfoxsoccermatchpass.comfoxsoccerplus.comfoxsoccerplus.netfoxsoccerplus.tvfoxsoccershop.comfoxsports-chicago.comfoxsports-newyork.comfoxsports-world.com foxsports.cl foxsports.cofoxsports.co.ve foxsports.comfoxsports.com.arfoxsports.com.bofoxsports.com.brfoxsports.com.cofoxsports.com.ecfoxsports.com.gtfoxsports.com.mxfoxsports.com.pefoxsports.com.pyfoxsports.com.uyfoxsports.com.ve foxsports.gtfoxsports.info foxsports.netfoxsports.net.br foxsports.pe foxsports.sv foxsports.uyfoxsports2.comfoxsportsflorida.comfoxsportsgo.comfoxsportsla.comfoxsportsnetmilwaukee.comfoxsportsneworleans.comfoxsportsracing.comfoxsportssupports.comfoxsportsuniversity.comfoxsportsworld.comfoxstudiolot.com foxsuper6.com -foxtel.com foxtel.com.aufoxtelevisionstations.com  foxtv.com foxtvdvd.com  foxuv.comfoxweatherwatch.com  fssta.com -fxn.ws fxnetwork.comfxnetworks.com bentobox.tv kicu.tv ktvu.commyfoxsanfran.comafewmomentswith.com -anidom.comcasoneexchange.comcoronavirusnow.com -fse.tvgeraldoatlarge.comgooddaychicago.com joeswall.comkilmeadeandfriends.commaskedsingerfox.com -my13la.com -my20dc.commy20houston.com -my29tv.com my45.com  my9nj.commyfoxatlanta.commyfoxaustin.commyfoxboston.commyfoxcharlotte.commyfoxchicago.com myfoxdc.commyfoxdetroit.com myfoxdfw.commyfoxhouston.commyfoxhurricane.com myfoxla.commyfoxlosangeles.commyfoxlubbock.commyfoxmaine.com myfoxny.commyfoxorlando.commyfoxphilly.commyfoxphoenix.commyfoxtampa.commyfoxtampabay.commyfoxtwincities.com myfoxzone.com myq2.comnewsnowfox.comorlandohurricane.comparadisehotelquizfox.com q13.com -q13fox.comrealamericanstories.comrealamericanstories.inforealamericanstories.netrealamericanstories.orgrealamericanstories.tvrealmilwaukeenow.com rprimelab.comshopspeedtv.comsoccermatchpass.comspeeddreamride.comspeedfantasybid.comspeedracegear.com speedxtra.comteenchoice.com testonfox.comtheclasshroom.comthefoxnation.comthegeorgiascene.comwhatthefox.comwhosthehost.com wofl.tvwoflthenewsstation.com wogx.com foxnewsplayer-a.akamaihd.netfreebeacon.comgannett-cdn.com gannett.comgannettdigital.com usatoday.comusatodaynetworkservice.comglobalvoices.orgglobalvoicesonline.org  guo.media  gnews.org gtv.org gtv1.org g-tvapp.com hk01.com hkcnews.com hkopentv.com fantv.hkhongkongfp.comhuffingtonpost.cahuffingtonpost.co.ukhuffingtonpost.co.zahuffingtonpost.comhuffingtonpost.com.auhuffingtonpost.com.mxhuffingtonpost.dehuffingtonpost.eshuffingtonpost.frhuffingtonpost.grhuffingtonpost.inhuffingtonpost.ithuffingtonpost.jphuffingtonpost.kr huffpost.comhuffpostarabi.comhuffpostbrasil.comhuffpostmaghreb.com inmediahk.netbusinessinsider.co.zabusinessinsider.combusinessinsider.com.aubusinessinsider.com.plbusinessinsider.debusinessinsider.esbusinessinsider.frbusinessinsider.inbusinessinsider.jpbusinessinsider.mxbusinessinsider.mybusinessinsider.nlbusinessinsider.sginsider-intelligence.com insider.cominsiderintelligence.com jfengtime.com landofhope.tvlocalpresshk.com -ltn.com.tw yes123.com.tw macrumors.com matters.news matters.one -medium.commedium.systems -myradio.hkmyradio.com.hkdreamworks.com cnbc.com -cnbcfm.com  msnbc.com -nbc.co nbc.com nbcnews.comnbcolympics.com nbcsports.com -nbcuni.comnbcuniversal.com s-nbcnews.com peacocktv.comuniversalorlando.comuniversalpictures.com&"universalpicturesinternational.com!universalstudioshollywood.com newscorp.com dowjones.com dowjones.iodowjoneson.com wsj.com wsj.net barrons.comandysparis.com newsmax.com -newsmax.in newsmaxtv.com newyorker.com -nikkei.com  nikkei.jp nikkei.co.jp now.com -now.com.hk nowe.com -now-tv.comnow-ashare.com nyt.com nyt.net nytcn.me  nytco.com nytimes.com nytstyle.comtimestalks.com aweencore.com  awetv.comherringnetwork.com oanencore.com oann.compassiontimes.hk qmap.pubrealvision.com reuters.com -reuters.tvreutersmedia.net rfa.org -rfi.fr rsf.org -rt.com etvonline.hk rthk.hk -rtings.com scmp.com -i-scmp.comshuziyimin.orgsinoinsider.com slashdot.orgsky sky.com skyassets.comskyoceanrescue.comskyoceanrescue.deskyoceanrescue.it skysports.com skysports.fr skysports.ieskysportsonline.comskysportsracing.comsoundofhope.org theguardiandev-guardianapis.comdev-theguardian.com -gu-web.netguardianapis.comguardianapps.co.uk -guim.co.uktheguardian.comtheguardiandns.comtheinitium.com  init.shopthenationalpulse.comthestandnews.comdreamteamfc.com  page3.comsunbingo.co.uk talksport.comthescottishsun.co.uk thesun.co.uk  thesun.ie thetype.cloud -cn thetype.comtypeisbeautiful.com udn.com -udn.com.twudnfunlife.com  unwire.hkamerikaninsesi.comamerikaninsesi.orgamerikaovozi.comamerikayidzayn.comamerikiskhma.com bbg.govdandalinvoa.com darivoa.comdengeamerika.comdengiamerika.comglasamerike.netgolos-ameriki.ru insidevoa.com pashtovoa.com persagg.comradiyoyacuvoa.com urduvoa.com voa.govvoaafaanoromoo.comvoaafrique.comvoabambara.com voabangla.comvoacambodia.comvoacantonese.comvoachinese.comvoadeewanews.comvoadeewaradio.com voahausa.comvoaindonesia.com voakorea.comvoalingala.comvoandebele.com voanews.com -voanews.euvoanoticias.com voanouvel.comvoaportugues.com voashona.com voasomali.comvoaswahili.com voathai.comvoatibetan.comvoatibetanenglish.comvoatiengviet.com voatour.comvoazimbabwe.comzeriamerikes.com cstatic.net -curbed.com  eater.comfunnyordie.com meridian.net polygon.com -racked.com -recode.net sbnation.com theverge.com vox-cdn.com vox.comvoxcreative.comvoxfieldguide.com voxmedia.com -voxops.net warroom.orgwashingtonpost.com watchout.tw -waa.tw -wenzhao.castartupschool.orgycombinator.com -zaobao.com zaobao.com.sg  zaobao.sg zerohedge.com zoomingin.tv aomedia.org archive.org  atypi.org -change.org ccstatic.orgcreativecommons.orgcreativecommons.engineeringlicensebuttons.net csis.orgcsis-prod.s3.amazonaws.comemojipedia.orgworldemojiawards.comworldemojiday.com xn--yt8h.laglobalsecurity.org ieee.org ieee.tvtryengineering.org ieee-ccnc.org ieee-ecce.org ieeemce.orgstandardsuniversity.orgieee-region6.org ieee.ca ieeer10.org -ieeer5.org -ieeer8.org ieeeusa.orgieeefoundation.orgieee-into-focus.org ieee-vics.org ieeeday.orgdatasheets360.comglobalspec.com ieee-isto.orgtechstreet.com computer.org -comsoc.org embs.org emcs.org grss-ieee.org ieee-aess.org ieee-cas.org ieee-ceda.orgieee-edusociety.org ieee-ies.org ieee-ims.org ieee-npss.org ieee-pels.org ieee-pes.org ieee-ras.orgieee-sensors.org ieee-tems.org ieee-uffc.org ieeeaps.org ieeecsc.org ieeecss.org ieeedeis.orgieeemagnetics.org ieeenano.org ieeeoes.org ieeesmc.orgieeesystemscouncil.org  itsoc.org mtt.orgphotonicssociety.orgsignalprocessingsociety.orgtechnologyandsociety.org vtsociety.org ietf.org itu.int nist.gov time.gov openmaps.orgopenstreetmap.comopenstreetmap.netopenstreetmap.orgopenstreetmaps.org osm.orgosmfoundation.orgstateofthemap.comstateofthemap.orgrolfoundation.orgrolsociety.org  smpte.org -un.org -w3.org who.int wto.org  1pondo.tv -91porn.com  avgle.com avn.com beeg.com bongacams.com -bcicdn.com bcrncdn.com -btt804.com -btt904.com -bttzyw.com -bttzyw.net t66y.comchaturbate.com e-hentai.org ehgt.org ehtracker.org -ehwiki.org exhentai.orghentaiverse.org eporner.comfanhaodian.com  hegre.com heydoga.com  heyzo.com hqporner.com -javbus.com javfinder.sh findercdn.me o0-2.com  javhd.comjavlibrary.com -javwide.tv javwide.com kindgirls.comlivejasmin.com dditsadn.com dditscdn.com -metart.commetartnetwork.com metcams.comnaiadsystems.com  nsimg.net netflav.com  pigav.com -pornhd.com -phncdn.com phprcdn.com pornhub.compornhubpremium.com  press.vin redtube.com scoreland.comcdn77.scoreuniverse.com spankbang.comtheporndude.com  tube8.comvaginacontest.com  x-art.com xhamster.comxhamsterlive.com  xhcdn.comwidgets.stripst.com#collector.xhamster.com -ads xnxx.com xnxx-cdn.com xvideos.comxvideos-cdn.com youjizz.comyjcontentdelivery.com youporn.comaccuweather.comgetadblock.comadblockplus.org addthis.comaddthiscdn.comaddthisedge.com addtoany.comadguard-vpn.com adguard.com apkpure.com cdnpure.com pureapk.com -winudf.com -arphic.com arphic.com.cn arphic.com.twifontcloud.com -bit.ly  bitly.com bitly.is bitwarden.combigbuckbunny.org blender.orgelephantsdream.org videojs.com bcove.video bcovlive.io bcvp0rtal.com boltdns.netbrightcove.combrightcove.netbrightcove.servicesbrightcovecdn.combcovlive-a.akamaihd.netbcsecure01-a.akamaihd.netbrightcove.imgix.net buymeacoff.eebuymeacoffee.comcloudconvert.comcontentful.com ctfassets.net cutt.lydigitalocean.comdigitaloceanspaces.com -disqus.com disquscdn.comdisqusservice.comduckduckgo.com duck.com -ddg.ggspreadprivacy.com easylist.to lanik.usentermediadb.netentermediadb.org -arcgis.comarcgisonline.com esri.com evernote.com faststone.org -feedly.com  figma.com gofundme.com gravatar.com botstop.com hcaptcha.comhcaptchastatus.com  heroku.co -heroku.com  heroku.me herokuapp.com herokucdn.comherokucharge.com herokussl.comheroku-app.comheroku-charge.com codei.sh -codeish.co -codeish.io iina.ioinfrapedia.com inoreader.com is.gdv.gd -jwpcdn.comjwplatform.com jwplayer.com -jwpltx.com -jwpsrv.comlongtailvideo.comkindle4rss.com kodi.tv  kodi.wiki liberapay.com liberapay.org linguee.com  deepl.com -madshi.net  madvr.com  madvr.net madvrlabs.llc mapbox.cn -cn -mapbox.com -mega.co.nz mega.nz -mpv.io netlify.combitballoon.com  notion.sonotion-static.comopencollective.com osdn.net pastebin.com patreon.com pics.ee  picsee.co -picsee.pro pise.pw pros.ee -pse.is ping.pe protonmail.chprotonmail.comprotonstatus.com pm.me quip-cdn.com quip.comquipelements.com -reabble.cn -cn reabble.com rb.gy rebrandly.com reurl.cc -dns.sb ip.sb loli.net sb.sb sm.msu.nu -www.sb sharethis.com myshopify.com shopify.com shopify.devshopifycdn.comshopifystatus.comshopifysvc.com shorturl.atsourceforge.comsourceforge.net -sf.net fsdn.com startpage.com ixquick.com steemit.com svp-team.com test-ipv6.com ctan.orglatex-project.org -miktex.org tug.org tidelift.com tinyurl.com -trello.com trellocdn.com -tumblr.com -now.shvercel-dns.comvercel-status.com -vercel.app -vercel.comzeit-world.co.ukzeit-world.comzeit-world.netzeit-world.org zeit.co zeit.sh zeitworld.com webflow.com wix.com  wixmp.com wixsite.com wixstatic.comvideopress.com w.org wordpress.com wordpress.org wordpress.tv wp-themes.com -wp.com  wpvip.com -zeplin.dev  zeplin.io zoho.com zoho.com.au zoho.eu zoho.in zohocdn.comzohomeetups.comzohomerchandise.comzohopublic.comzohoschools.comzohostatic.com zohostatic.inzohouniversity.comzohowebstatic.com zoom.com zoom.com.cn zoom.usalpinelinux.org anaconda.com anaconda.org conda.io android.comandroidify.com -apache.orgappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cncalendarserver.orgapple-cloudkit.com cups.orgdeveloper.apple.comdevimages-cdn.apple.comdevstreaming-cdn.apple.comfoundationdb.orgmacosforge.org macruby.com macruby.net macruby.orgapple-mapkit.complaygrounds-cdn.apple.com carekit.orgresearchandcare.orgresearchkit.cn -cnresearchkit.com.cn -cnresearchkit.hkresearchkit.netresearchkit.orgresearchkit.tvservicetalk.io -webkit.org webkitgtk.org wpewebkit.orgwebobjects.com-)appleworldwidedeveloper.hb-api.omtrdc.net)%appleworldwidedeveloper.sc.omtrdc.net#p-events-delivery.akamaized.net archlinux.orgarchlinuxarm.org atom.io -badgen.netbootstrapcdn.comgetbootstrap.combraventures.com braveux.com -compass.isfightforux.comforthebadge.comstockagainstphotography.com launchpad.netlaunchpadlibrarian.comlaunchpadlibrarian.netlaunchpadlibrarian.org snapcraft.io snapstore.io -ubuntu.com -ubuntu.netubuntuforums.org canonical.com  cdnjs.com -centos.orgchocolatey.org clojure.org -cnpmjs.org -cygwin.com -cython.org dart.dev dartlang.org dartpad.dev -debian.org -dotdeb.org  deno.landcompose-spec.io -docker.com  docker.ioelixir-lang.org -elrepo.org embed.ly embedly.com playerjs.io -erlang.orgaccountkit.comatscaleconference.com botorch.org -buck.build buckbuild.comcomponentkit.org draftjs.org -f8.comfacebookappcenter.infofacebookappcenter.netfacebookappcenter.orgfacebookconnect.comfacebookdevelopergarage.comfaciometrics.com fasttext.cc fbf8.com fbinfer.com fblitho.com fbredex.com -fbrell.com flow.dev flow.org flowtype.org frescolib.org hacklang.org hhvm.commakeitopen.com mcrouter.net mcrouter.orgmessengerdevelopers.com -ogp.meopengraphprotocol.comopengraphprotocol.org  parse.com pyrobot.org  react.com reactjs.com reactjs.org recoiljs.org rocksdb.com rocksdb.net rocksdb.orgyogalayout.comdevelopers.facebook.com fastlane.cifastlane.tools getfedora.orgfedoraforum.orgfedoraproject.orgfedoramagazine.org -ffmpeg.org flathub.org flatpak.org flutter.devflutterapp.comfontawesome.comfortawesome.com freebsd.org  fury.blog fury.co fury.dev  fury.help fury.io gemfury.com -gentoo.orggetcomposer.org git-scm.com gitbook.com -gitbook.iogitbook.zendesk.com  npmjs.com npm.communitydependabot.com -git.io github.blog -github.comgithub.community  github.io githubapp.comgithubassets.comgithubhackathon.comgithubstatus.comgithubusercontent.com/+github-avatars.oss-cn-hongkong.aliyuncs.comFBgithub-production-release-asset-[0-9a-za-z]{6}\.s3\.amazonaws\.com -gitlab.com  gitlab.io -gitlab.netgitlab-static.net.*gitlab-assets.oss-cn-hongkong.aliyuncs.com gnu.org go-lang.com go-lang.net go-lang.org -go.dev  godoc.org -golang.com -golang.net -golang.org  consul.io hashicorp.comnomadproject.io  packer.io terraform.io vagrantup.comvaultproject.io haskell.orghaskellstack.org stackage.org brew.shhomebrew.bintray.com -01.org hyperscan.iointellinuxgraphics.comintellinuxgraphics.net -oneapi.comopenvinotoolkit.orgsnap-telemetry.io -acpica.com clearlinux.cn -cnclearlinux.org -ospray.net -ospray.org -ius.iojava java.com java.net -jenkins.io jetbrains.com jetbrains.net datalore.iokotlinlang.org  jfrog.com  jfrog.org bintray.com!d29vzk4ow07wi7.cloudfront.net -js.org jsdelivr.net jsdelivr.comcdn.jsdelivr.net -cn jupyter.org kali.org kubernetes.io -k8s.iolinuxfromscratch.org linuxmint.com lua.org macports.org mariadb.orgadaptivecards.ioapi-extractor.com -apisof.net appcenter.ms -blazor.netbotframework.comcodethemicrobit.comdevopsassessment.net dot.netgamesstack.comgraphengine.ioinsiderdevtour.com -jwt.ms microbit.orgmicrosoftadc.commicrosoftgamestack.commicrosoftiotinsiderlabs.commicrosoftreactor.cn -cn#microsoftreactor.com.cn -cnmicrosoftreactor.infomicrosoftreactor.netmicrosoftreactor.orgmicrosoftsilverlight.commicrosoftsilverlight.orgmicrosoftsilverlight.netmicrosoftsqlserver.com  mmdnn.commono-project.com msdn.commsinnovationchallenge.commspairlift.commsropendata.com  nuget.org nugettest.orgopentranslatorstothings.orgposhtestgallery.compwabuilder.comreactorms.com.cn -cn -sankie.netsqlserveronlinux.comtimelinestoryteller.comuwpcommunitytoolkit.comvisualstudio-staging.comvisualstudio.comvisualstudio.covisualstudio.co.ukvisualstudio.euvisualstudio.net vfsforgit.com vfsforgit.org vsassets.io xamarin.comdeveloper.microsoft.comdtlgalleryint.cloudapp.net poshtestgallery.cloudapp.net"psg-int-centralus.cloudapp.netpsg-int-eastus.cloudapp.net  mingw.org mongodb.com mongodb.org  mysql.com  nginx.com  nginx.org  nixos.orgnodesource.comnotepad-plus-plus.org -openai.com openresty.org openssl.org opensuse.org openwrt.orglede-project.org packagist.org pcre.org perl.org cpan.org metacpan.org phantomjs.org php.netpolymer-project.orgpolymerproject.orgpostgresql.org -python.orgpythonhosted.org pypa.io pypi.org qt.ioqt-project.org r-project.orgraspberrypi.org raspbian.orgreadthedocs-hosted.comreadthedocs.comreadthedocs.ioreadthedocs.org redis.io redislabs.com remirepo.net rpmfusion.org ruby-lang.org rubygems.orgscala-lang.org scala-sbt.orgscientificlinux.org scoop.sh -shields.io -sqlite.org askubuntu.comblogoverflow.commathoverflow.netserverfault.com sstatic.net stackapps.com stackauth.comstackexchange.com stackmod.blogstackoverflow.blogstackoverflow.comstackoverflow.emailstackoverflowbusiness.comstacksnippets.net superuser.com tex-talk.netthesffblog.commystrikingly.comstrikingly.comstrikinglycdn.comsublimetext.com -kernel.org  linux.org amp.devampproject.comampproject.netampproject.orgcdn.ampproject.org -cnamp.akamaized.netelectronjs.org -jquery.comjquerymobile.com jqueryui.com mochajs.org -nodejs.org openjsf.orglfai.foundationlinuxfoundation.org  unity.com unity3d.com!unityads.unity3d.com -ads#unityads.unitychina.cn -ads  unpkg.com -v8.dev v8project.org videolan.org vim.org wireshark.org x.orgxda-developers.com xda-cdn.com xposed.info yarnpkg.com zsh.orgacer-group.com acer.com acer.com.cn -cn typekit.com typekit.net10xfotolia.com 2o7.net acrobat.com adbecrsl.comadobe-aemassets-value.comadobe-audience-finder.com"adobe-video-partner-finder.com  adobe.com adobe.lyadobeaemcloud.comadobeaemcloud.netadobeawards.comadobecreativityawards.com adobedc.net adobedemo.com adobedtm.comadobeexchange.comadobeexperienceawards.com adobegov.comadobehiddentreasures.comadobejanus.com adobeku.comadobelogin.com adobeoobe.comadobeplatinumclub.comadobeprojectm.com adobesc.com adobesign.comadobesigncdn.comadobespark.com adobess.comadobestock.com adobetag.comadobetarget.comadobetcstrialdvd.comadobetechcomm.comadobetechcommcallback.comadobetechcommdemo.comadobexdplatform.comassetsadobe.com authorxml.combusinesscatalyst.com ccnsite.com  ccpsx.com -demdex.netdollarfotoclub.comdollarphotoclub.comdollarphotosclub.comdouwriteright.com echocdn.com echosign.com edgefonts.netenablementadobe.com  ftcdn.net gostorego.comimagineecommerce.commacromedia.commagentoliveconference.commarketing-nirvana.com omniture.com -omtrdc.net photoshop.complacesdocs.comsundanceignite2016.comworldsecureemail.comworldsecuresystems.com adobe.iodevelopria.com behance.nets2stagehance.com adobecc.com adobecce.comadobeccstatic.comcreativecloud.comcreativesdk.com cotolia.com ffotolia.com fiotolia.com foftolia.com fonolia.com fotiolia.com fotoiia.comfotolia-noticias.com -fotolia.cc fotolia.com -fotolia.tv fotolja.com fptolia.com gfotolia.com motolia.com photolia.nettenbyfotolia.com votolia.combluefootcms.com mageconf.commageconf.com.ua magento.com magento.netmagentocommerce.commagentomobile.commarketing-cloud.com marketo.co.uk marketo.com marketo.net -marketo.tvmarketodesigner.commarketolive.com -mktdns.commkto-c0100.com mktorest.com mktroute.commobilemarketo.com&"revenue-performance-management.com toutapp.comalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.com amazon-adsystem.com -adswholecitiesfoundation.orgwholefoodsmarket.comwholekidsfoundation.orgwholeplanetfoundation.orgaws -amazonimdb -kindle xn--cckwcxetdxn--jlq480n2rg yamaxun -zapposaboutamazon.cn -cnaboutamazon.co.ukaboutamazon.comaboutamazon.com.auaboutamazon.deaboutamazon.esaboutamazon.euaboutamazon.fraboutamazon.inaboutamazon.itaboutamazon.jpaboutamazon.pl alexafund.cn -cnalexafund.com.cn -cn amaaozn.comamazon-fashions.comamazon-jp-recruiting.comamazon-lantern.comamazon-launchpad.com  amazon.ae  amazon.ca amazon.cn -cn amazon.co.jp amazon.co.uk -amazon.com amazon.com.au amazon.com.br amazon.com.mx amazon.com.tr  amazon.de  amazon.es  amazon.fr  amazon.in  amazon.it amazon.jobs  amazon.jp  amazon.nl -amazon.red  amazon.sgamazonalexavoxcon.comamazonauthorinsights.comamazonbusiness.cn -cn!amazonbusiness.com.cn -cnamazonbusiness.orgamazonbusinessblog.comamazonchoice.cn -cnamazonchoice.com.cn -cnamazonchoices.cn -cn amazonchoices.com.cn -cnamazondevicesupport.comamazonfctours.comamazonianblog.comamazonimages.comamazoninspire.cn -cn amazoninspire.com.cn -cnamazonlaunchpad.cn -cnamazonlaunchpad.com"amazonlaunchpad.com.cn -cn amazonlending.com.cn -cn!amazonliterarypartnership.comamazonlumberyard.wang amazonnow.cn -cnamazonnow.com.cn -cn amazonsdi.comamazonstudiosguilds.comamazontrust.comamazonvideodirect.com amzn.com amzn.to  amznl.comassoc-amazon.cn -cnassociates-amazon.comgameon-masters.comgdansk-amazon.commedia-amazon.com primeday.cn -cnprimeday.com.cn -cn primeday.infosiege-amazon.comssl-images-amazon.comueberamazon.de -yamaxun.cn -cn yamaxun.comz.cn -cn audible.comacmvalidations.comacmvalidationsaws.comaesworkshops.comamazonaws-china.com amazonaws.com amazonaws.tvamazonworkdocs.cn -cnamazonworkdocs.com!amazonworkdocs.com.cn -cnamplifyapp.comamplifyframework.comaws-iot-hackathon.comawsautopilot.comawsautoscaling.com awsbraket.comawscommandlineinterface.comawsedstart.comawseducate.comawseducate.netawseducate.orgawsloft-johannesburg.comawsloft-stockholm.comawssecworkshops.com awsstatic.comawsthinkbox.com awstrust.comcdkworkshop.comcloudfront.netcontainersonaws.comelasticbeanstalk.comthinkboxsoftware.com".+\.awsdns-[0-9][0-9]\.co\.uk$.+\.awsdns-[0-9][0-9]\.com$.+\.awsdns-[0-9][0-9]\.net$.+\.awsdns-[0-9][0-9]\.org$".+\.awsdns-cn-[0-9][0-9]\.biz$!.+\.awsdns-cn-[0-9][0-9]\.cn$".+\.awsdns-cn-[0-9][0-9]\.com$".+\.awsdns-cn-[0-9][0-9]\.net$ kindle.cn -cn kindle.co.jp kindle.co.uk -kindle.com  kindle.de  kindle.es  kindle.fr  kindle.in  kindle.it  kindle.jpkindleoasis.cn -cnkindleoasis.comkindleoasis.com.cn -cnkindleoasis.infokindleoasis.jpkindleoasis.orgkindleoasis.uskindleoasisnews.comkindleproject.comamazonprimevideo.cn -cn#amazonprimevideo.com.cn -cnamazonprimevideos.comamazonvideo.ccamazonvideo.comprime-video.com primevideo.ccprimevideo.comprimevideo.infoprimevideo.org primevideo.tv+images-cn.ssl-images-amazon.com -cn-!images-cn-8.ssl-images-amazon.com -cn amd.comamdfanstore.com -radeon.comiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -ads100beatscheap.com1monsterbeatsbydreus.com2013beatsbydrdreonline.com2013beatsbydrdreshop.com2013beatsbydreshop.com2013beatshdcybermonday.com2013cheapestbeats.com2013newbeatsworld.com2013pascherbeatsbydre.com2014cheapbeatsbydre.com 2beatsdre.com2buybeatsbydre.com2drdrebeats.com40shopping.com4beatsbydre.com5beatsbydre.comaaagradeheadphones.com -aanaan.comabcheadphones.comabeatsbydrdre.com aeasyshop.com -airmay.com -ajtalk.com albeats.com annstores.netaokwholesale.netaucheapbeats.comaudiobeatsau.comaudiobeatsbydre.comaudiomonsterbeatsonline.comauricularemonsterbeats.com#auricularesbeatsbaratosshop.comauricularesbeatsmarkt.comausbeatsbydrdre.combag-glasses1.com bandgirlz.com baxsound.com bbestmall.com -beatbd.combeatbydre2013.combeatbydremonster.com!beatbydreheadphonesonsale.combeatbydrekopen.combeatbydreuk2014.combeatdrdres.combeatfactoryoutlets.combeatmonstersaustralia.netbeats-by-dre-australia.combeats-bydrdre.netbeats-bydre-mall.combeats-bydrecheapsale.combeats-bydreoutletonline.combeats-bydreoutletsale.combeats-bydreoutletsale.netbeats-bydreoutletssale.netbeats-bydrestore.combeats-bydreuk.combeats-deal.combeats-dre-us.com"beats-headphones-buy-cheap.combeats-headphones.usbeats-sale.combeats-seller.combeats-soaho.com  beats1.cc beats1.cn -cn  beats1.tv beats1.com.cn -cn beats123.com beats2.com.cn -cn beats4.cn -cn -beats4.netbeats4outlets.combeats4salecheap.combeatsallsale.combeatsaudifonos.combeatsaudiobydre.combeatsaudiobydre.netbeatsaudios.netbeatsbeatsmonster.combeatsbestdeals.combeatsblackfridaydeals.netbeatsblackfridayretails.combeatsboxingdayuksale.combeatsbydrdre-headphones.combeatsbydrdre-officials5.combeatsbydrdre-online.combeatsbydrdre-onsale.combeatsbydrdre-store.combeatsbydrdre-store.usbeatsbydrdre4sale.combeatsbydrdrebiz.combeatsbydrdrecustom.combeatsbydrdredanmark.combeatsbydrdremall.combeatsbydrdreoutlet.combeatsbydrdres.combeatsbydrdresale.netbeatsbydrdrestore.combeatsbydrdreus.com!beatsbydre-beatsheadphone.combeatsbydre-chen.combeatsbydre-club.combeatsbydre-headphones.com!beatsbydre-headphonesshop.combeatsbydre-mall.combeatsbydre-outlet.combeatsbydre-outletsale.netbeatsbydre-outletstore.combeatsbydre-sell.combeatsbydre-store.combeatsbydre-studio.combeatsbydre-us.com beatsbydre.jpbeatsbydre2081.combeatsbydre411.combeatsbydre4usales.combeatsbydreauofficial.combeatsbydreausale.net beatsbydreaustralia-sale.com!beatsbydreaustraliaonline.com"beatsbydreaustraliaonlines.com beatsbydreaustraliasales.combeatsbydrebeatsby.com!beatsbydreblackfriday2013.com beatsbydreblackfridaypro.combeatsbydreboxingdayca.combeatsbydrecasquesfr.com#beatsbydrecheap-outletstore.combeatsbydrecheaper.com"beatsbydrecheaponlinesales.combeatsbydrecolors.com beatsbydrecustomwireless.com&"beatsbydrecybermondaydeals2013.com&"beatsbydrecybermondaydeals2013.netbeatsbydredanmarks.com"beatsbydredealsblackfriday.com"beatsbydredealscybermonday.combeatsbydrediscount.com beatsbydrediscountonline.netbeatsbydredr.combeatsbydreexecutivesale.com beatsbydreformall2013-nl.com beatsbydreforsalesonline.com beatsbydreforshop2013-nl.combeatsbydreforstore.combeatsbydreforyououtlet.combeatsbydrefr.com!beatsbydrefrcasquepascher.combeatsbydrefriday.combeatsbydregot.combeatsbydrehd.combeatsbydrehd.netbeatsbydreheadphones-nz.combeatsbydreheadphonesolo.combeatsbydrehut.combeatsbydreinexpensive.combeatsbydreireland-sales.com beatsbydreirelandonlines.combeatsbydreirelandsale.com beatsbydreirelandsonline.combeatsbydremall2013.com"beatsbydremonsteraustralia.combeatsbydrenls.combeatsbydrenorge1.net!beatsbydreofficialdanmark.combeatsbydreoksale.combeatsbydreol.combeatsbydreonlie2013-nl.com!beatsbydreonlines-ireland.combeatsbydreonlines-uk.combeatsbydreonlinesale-nz.combeatsbydreoordopjes.combeatsbydreoslo.combeatsbydreoutletsale.combeatsbydreoutletscheap.combeatsbydrerealstore.combeatsbydres-shop.combeatsbydresale-uk.combeatsbydresalemall2013.combeatsbydresales.us'#beatsbydresalesonline-australia.com beatsbydresaleonlines-nz.combeatsbydresdanmark.netbeatsbydreshop-uk.combeatsbydreshops.netbeatsbydresingapores.combeatsbydresingaporesale.combeatsbydrespeakers.combeatsbydresold.com%!beatsbydresolohdonline-canada.combeatsbydresonline-nz.combeatsbydrestorevip.combeatsbydressale.combeatsbydressolo.combeatsbydresstudio.com"beatsbydrestudio-australia.combeatsbydrestudio.combeatsbydretoutlet.combeatsbydreuk.combeatsbydreuk.netbeatsbydreus.combeatsbydrevipde.combeatsbydrew.combeatsbydrsmonsterinusa.combeatsbymusic.netbeatsbysdrbre.combeatsbysdrdres.combeatscasque-france.combeatscheap-nz.combeatscheap-uk.combeatscheapforsale.combeatscollection2014.combeatscustomblackfriday.combeatsdanmark2013.combeatsdrdre-headphones.combeatsdrdre-it.combeatsdrdre-solo.combeatsdrdre2014.combeatsdrdrecuffie.netbeatsdrdrekaufenschweiz.netbeatsdrdreneon.combeatsdre-monster.com beatsdre.netbeatsdre4cheap.combeatsdrecheap.combeatsdredreheadphones.combeatsdreforsale.combeatsdreinau.combeatsdremonster-uk.com!beatsdrenewcolorful4usale.combeatsdreoutletsale.combeatsdresale2013.combeatsdresalestore.combeatsdresolo2013.combeatsdreus.combeatsearbudsheadphoness.combeatsfacstore.combeatsfactoroutlets.combeatsfactorycollection.combeatsfactoryoutles.combeatsforcheap-usa.combeatsforme.combeatsfranceofficiel.combeatselectronic.netbeatselectronics.com -beatsep.cn -cn beatsep.com beatsep.net beatsheadphones-discount.combeatsheadphones1.combeatsheadphones2u.combeatsheadphonesale.combeatsheadphonesdealer.combeatsheadphonesforcheap.netbeatsheadphonesonline.combeatsheadphoness.combeatsheadphonestudio.combeatsheadphonesusamall.combestshoesale2014.netbeatshopuk.combeatsincanada.combeatsinsingapore.combeatsireland.net beatsjoy.combeatsmonstersales.combeatsmusic.combeatsmusic.wang beatsneon.combeatsnzsale.com beatsodre.combeatsofdre-usa.combeatsonblackfriday2013.com beatsone.netbeatsoutlet.netbeatsoutletanytime.combeatsoutletonlines.combeatspascher-bydre.combeatspascher-fr.netbeatspascher.netbeatspaschers.netbeatspillnewcolor.combeatspills.combeatspromonsterjp.combeatspromotions.combeatssales.combeatssaleus.combeatssaustraliabuy.combeatssbyaustralia.combeatssbydredanmark.combeatsshop-uk.combeatsshop-usa.combeatsshopstore.combeatssingapores.combeatssingaporeshop.com beatsstudiohodetelefoner.combeatsstudiosite.combeatstoreusa.com beatsua.combeatswholesale.usbeatswirelesscheap.combeatswirelesscuffie.combegin-trade.combestbeats4u.com -biitii.comblackfridaydrebeatsnew.comblackfridaydrebeatsshop.combloommicroventures.com bookonsky.net -bossae.combrandeasygo.combrandelectronic.combrandproducts1688.com brands098.combusinesswebwise.combuy-from-shanghai.combuybeatsbydre-uk.combuybeatsheadphonesbydre.combuycheapbeatsbus.combuycheapbeatsbydre.combuycheapbeatsbydreshop.combuycheapbeatsdreuk.combuydrdrebeatbox.combuyminibeatbox.combuyonlineheadphones.combuyshoponly.combuzzardflapper.combybeatsdre.combydrebeats.comcanada-beatsbydre.com casque-fr.com casquebeatsbydrdresolohd.comcasquebeatsdocteurdre.comcasquebeatsdre2013.comcasquebeatsenligne.comcasquebeatsfracheter.comcasquebeatsmer.netcasquebeatsofficiel-fr.comcasquebeatspascher2013.comcasquebeatssolo.netcasquedrdrebeatssfr.comcasquemonsterbeats.com#casquemonsterbeatsbydre2013.comcasquesbeatsaudio.comcheap-beats-by-dre.netcheap-beatsbydre.comcheapbagshoes.com cheapbeats.uscheapbeats365.comcheapbeats4sale.netcheapbeatsaustraliasale.comcheapbeatsbus.comcheapbeatsbydr.comcheapbeatsbydrdrepro.comcheapbeatsbydre-au.comcheapbeatsbydre99.comcheapbeatsbydrefau.comcheapbeatsbydremall.comcheapbeatsbydremonster.comcheapbeatsbydrenz.netcheapbeatsbydreonsale.com cheapbeatsbydreoutlet-nz.comcheapbeatsbydreoutlet.com"cheapbeatsbydreoutlets2013.comcheapbeatsbydresale.comcheapbeatsbydreshop.comcheapbeatsdrdresolo.com$ cheapbeatsbydrestudioedition.com"cheapbeatsbydrestudioutlet.comcheapbeatsdrestudios.comcheapbeatsheadphone2014.comcheapbeatsheadphones.uscheapbeatsie.comcheapbeatsla.comcheapbeatssale4u.comcheapbeatsshopbydre.comcheapcustombeatsbydre.comcheapdrdrebeats8.comcheapdrdrebeatsca.comcheapdrebeats8.netcheaperbeatsbydresale.comcheapestbeatsdrdre.comcheapheadphonesland.comcheapheadphonessale.comcheapheadsetmall.comcheapheadsetshop.com cheapmonsterbeatsbydrdre.com!cheapmonsterbeatsheadsets.com cheapmonstersbeatsonsale.comcheapmonsterbeatssale.comcheapmonsterbeatsusa.uscheapnewbeatsbydre.comcheapsalemonster.comcheapshoesvip.comcheapwireless04.comcheapwirelessbeats.comcheerwholesale.uschihair-straightener.comchinaclothesstore.comciattackers.com -cmhalq.com cnshopin.comcoolmonster.netcozydrdrebeats.com cqcorea.comcuffiesaldi.comcustombeatsbydrebuy.comcustombeatsdeals.comcustombeatsforcheap.comcustombeatsny.comcustombeatssbydreus.comcustomdrdrebeats.comcustomizedbeatbydre.comcustomizedbeatsbydre.comcustomizedbeatsdre.comcybermondaybeats4sale.comdanemarket.comdanmarkbeatsbydrdre.comdctbeatsbydre.comdealsbeatsblackfriday.comdetaliczny.comdiscountbeatsbydre-us.comdiscountbeatsstore.comdkbeatsbydre.comdrbeatsukmart.comdrdre-beats.comdrdrebeats-chen.comdrdrebeats-headphone.comdrdrebeats-usa.comdrdrebeatsale.comdrdrebeatsbillig.comdrdrebeatsdesale.comdrdrebeatsdiscount.comdrdrebeatsforu.comdrdrebeatsretail2013.comdrdrebeatssale7.comdrdrebeatsuk.comdrdreheadphonebeats.comdrdreheadphonekey.comdrdreheadphonesusstore.comdrdremonster-beats.comdrdremonsterdre.comdrdreprobeatssale.com drdrefnac.comdrebeats-australia.comdrebeats-france.comdrebeats-monster.com!drebeats-monsteraustralia.comdrebeats-monsterusa.comdrebeats-singapore.comdrebeats-singapore.netdrebeats-singaporecheap.comdrebeats-singaporecheap.netdrebeats-solo.comdrebeats-studio.comdrebeatsaustralia-cheap.comdrebeatsaustralia-cheap.netdrebeatsbuy.comdrebeatsbydreoutlet.comdrebeatscanada.comdrebeatsdeutschland.netdrebeatsforsaleus.comdrebeatsheadphones-nz.comdrebeatshome.comdrebeatsoutletstore.comdrebeatsoldes.comdrebeatspill.comdrebeatssite.com#drebeatssolocybermondaysale.comdrebeatsstudio2013.comdrebeatstudio.com drebyby.comdremonsterbeatsoutlets.comdreprobeats.com dreprofy.com -dtsell.comearphonescheapest.comebuyheadphones.comesbeatsbydrebuy.cometheadphones.comfamosascalvas.comfindmybeats.comfr-beatsbydrestore.comfrancecasquebeatssolde.comfrcasquesbeats.com free-aa.com free-sns.com frishoes.comgamebeforethegame.comgetdrebeatssale.comgmnetworks.net gobeatsye.comgobuyonlinestore.net goodsdunk.netheadphonepubs.comheadphones-cool.comheadphones-dre.com headphones-outlet-online.comheadphonesbeatsaudio.comheadphonesbeatsbydre.comheadphoneses.comheadphoneshome.comheadphoneshotsales.comheadphonesol.comheadphonesretailer.comheadphonessupply.comheadphonezip.comheadset987.comhellokittybeats.com highbolt.net highdefinitionbeatsbydre.com hipaa6020.com hipzoom.nethonawalaan.comhotbeatsonsale.comhotmonsterbeats.comhottestheadphonesonline.com  hrsaz.com ibeats-uk.comibeatsbydre.ccibeatsbydre.comigoshopping.net illianacomputerrecycling.comimonsterbeats.comincentivetravelgifts.comioffergift.com islamiccenterofnewlondon.comitaliabeatsbydrdre.com jkbeats.comkickshatchannel.comkobbeatssbydredk.comkopfhorergunstigshop.comleecountytimes.com lexuemei.comlovebeatsdr.com lovemarca.comluckyissue.com -lyzsxx.com mairbeats.com -makeeu.commallheadphone.commbeats-tech.commbheadphone.com -micstl.commmonsterheadphones.netmonbeats2013.commonster-beats-by-dr-dre.com monster-beats-headphones.commonsterbeats-cheap.commonsterbeats-headphone.commonsterbeats-onsale.commonsterbeats-solo.commonsterbeats-solo.netmonsterbeats365buy.commonsterbeats8beatsbydre.commonsterbeatsale.commonsterbeatsalestore.commonsterbeatssalg.commonsterbeatsau.commonsterbeatsbydrdre-nz.commonsterbeatsbydrdre-usa.com!monsterbeatsbydrdrecanada.com!monsterbeatsbydrdrestudio.commonsterbeatsbydre-nz.commonsterbeatsbydre-usa.commonsterbeatsbydre2015.com'#monsterbeatsbydreaustraliacheap.com!monsterbeatsbydrebilligde.com monsterbeatsbydrefactory.commonsterbeatsbydrenew.commonsterbeatsbydres.commonsterbeatsbydreshop.commonsterbeatscasques.commonsterbeatscommunity.commonsterbeatsdrdrecheap.commonsterbeatsfactory.netmonsterbeatsfinland.commonsterbeatsforsale.commonsterbeatsheadphone.commonsterbeatsheadphones.netmonsterbeatshere.commonsterbeatsitaly.commonsterbeatsmall.commonsterbeatsnegozi.netmonsterbeatsok.com!monsterbeatsonlinestoreuk.commonsterbeatsoutlet.usmonsterbeatspascher.netmonsterbeatsru.commonsterbeatssales.commonsterbeatsshops.netmonsterbeatsstore.com#monsterbeatssydneyaustralia.commonsterbeatstang.commonsterbeatstienda.commonsterbeatstudio.commonsterbydrebeat.com!monstercasquebeatspascher.netmonstercheapbeatss.commonsterdrebeats-canada.netmonsterdrebeats-usa.commonsterdrebeats-usa.netmonsterheadphone.netmonsterproduct.netmonstersbeatbydres.commonstersdebea.commonstershopcheapbeats.netmoreheadphones.com -ms4dre.commybeatsbydrestudio.commybeatsbydreuk.commybeatscheapbydre.commyferrariheadphones.comnamemybeats.com neborder.comnegozimonsterbeats.comnegoziomonsterbeats.comnewbeatsblackfriday.com newbemany.comnewdrediscount.comnewestbeatsbydre.comnewlysprung.netnewmonst1erbeatsto1re.com!newmonsterbeatsheadphones.comnewschristmasshopping.comnflhotsalejerseys.com nicesshop.netofferairjordanlebron.comofficialbeatsbydreshop.comofficialbeatsbydrestore.comofficialdrdre.comofficialheadphone.com ogameblog.comonbeatsbydresale.com onlinemonsterbeatsonsale.com onsalekey.comonsaletrend.comoutletbeatsshop.compascherbeatsmonster.compaulsimon-music.comperfectkickz.netpersoneelsland.compicknicekicks.netpickshoesclothes.com pillbeatsblackfridaysale.compocketbiketrader.compowerbeats2wireless.compowerbeatsbydrdre.compowerbeatsbydre.compromonsterbeatsbydre.comprostudiobeatscybersale.com  rbsgr.comrepsneakermall.com repswing.com!salebeatslasteststyle4you.comsaleblackfridaydrebeats.comsalecheaphandbags.com samebags.com sanvaras.com -scnshop.cc -shoers.com shoestop2.comshop-headphones.netshopbeatsdre.com shopbydre.comshopcustomizedbeats.comshopdrebeats.comshopmonsterbeats.comshopping-days.netshoppinguheadphones.comsneaker666.comsneakerpage.netsneakerskick.comsolostudioksale.com soso7778.comspecialtyheadphones.comspiritclubs.comstraightouttasomewhere.comstudiobeatsbydrdre.comsulwerphoto.comsunglassessale2014.comsuperearsenjoy.comsupplybestjerseys.comsustainthesound.comsverigebeatsbydrdre.com szcheapmonsterheadphones.comszwinnertechnology.com takegoto.comthe-monster-beats.comthebeatsbydre.comthebeatsbydre.netthebeatsheadphonesale.com -theuab.nettopbeatsbydrdreoutlet.comtopbeatsdealer.comtopbeatsforsale.com tradevip1.com tteshop.com ubnw.net -unstyle.us&"usa-beatsbydreheadphonesonsale.net  v-has.com vip-beats.comvipbeatsbydre.comvipheadphones.com vipshoes2.com  wasdj.comwholesale-exporter1.comwholesalediscountpurses.comwholesalefine.comwholesaleonlinemart.comworldcoinpay.comyourmonsterbeats.com%!beatselectronicsllc.tt.omtrdc.netapple-icloud.cn -cnappleicloud.cn -cnapplemx-icloud.comicloud-apple.cn -cnicloud-content.comicloud-isupport.com  icloud.ch -icloud.com icloud.com.cn -cn  icloud.de  icloud.ee  icloud.fi  icloud.fr  icloud.hu  icloud.ie  icloud.is  icloud.jp  icloud.lv icloud.net.cn -cn  icloud.om -icloud.org  icloud.pt  icloud.ro  icloud.se  icloud.si  icloud.sk  icloud.vn icloudads.neticloudapple.cn -cn icloudbox.neticloudbrowser.net icloude.comicloudhome.comicloudmail.neticloudmusic.net icloudnet.net icloudpay.net icloudo.com -icloudo.de icloudo.net icloudos.de icloudos.neticloudsecure.neticloudsetup.comios-icloud.com -me.com myicloud.net mylcloud.net wwwicloud.com!www-cdn.icloud.com.akadns.net!12diasdepresentesdeitunes.com12diasderegalosdeitunes.cl12diasderegalosdeitunes.co!12diasderegalosdeitunes.co.cr!12diasderegalosdeitunes.co.ni!12diasderegalosdeitunes.co.ve12diasderegalosdeitunes.com"12diasderegalosdeitunes.com.co"12diasderegalosdeitunes.com.hn"12diasderegalosdeitunes.com.ni"12diasderegalosdeitunes.com.ve12diasderegalosdeitunes.cr12diasderegalosdeitunes.gt12diasderegalosdeitunes.hn12diasderegalosdeitunes.pe 12joursdecadeauxdeitunes.comapple-itunes.cn -cn itun.es itunbes.com ituneas.comitunes-apple.cn -cnitunes-radio.net  itunes.ca  itunes.co itunes.co.th -itunes.com  itunes.hk  itunes.mx -itunes.org -itunes.rio  itunes.usitunes12days.comitunesapple.cn -cnitunesessentials.comitunesfestivals.comitunesiradio.comituneslatino.comituneslogin.netitunesmatch.comitunesparty.comitunesradio.cn -cnitunesradio.comitunesradio.com.cn -cnitunesradio.rioitunesradio.tvitunesradio.twitunesstore.co itunesu.com itunesu.net iutunes.com wwwitunes.comitunes-apple.com.akadns.net apple apple.ae apple.at apple.be apple.bg apple.bs apple.ca apple.ch apple.cl apple.cmapple.cn -cn apple.co apple.co.cr apple.co.hu apple.co.jp apple.co.kr apple.co.mz apple.co.nz apple.co.th apple.co.uk  apple.com apple.com.af apple.com.au apple.com.bo apple.com.br apple.com.cn -cn apple.com.co apple.com.de apple.com.do apple.com.gr apple.com.gy apple.com.jm apple.com.lk apple.com.mg apple.com.mx apple.com.my apple.com.pa apple.com.pe apple.com.pl apple.com.pr apple.com.pt apple.com.py apple.com.sg apple.com.sv apple.com.tr apple.com.tw apple.com.uy apple.cz apple.de apple.dk apple.ee apple.es apple.eu apple.fi apple.fr apple.hamburg apple.hn apple.hr apple.hu apple.ie apple.in apple.is apple.it apple.jo apple.jp apple.kr apple.lk apple.lt apple.lv apple.me apple.my  apple.net apple.net.gr apple.nl apple.no apple.pk apple.pl apple.pt apple.ro apple.rs apple.ru apple.sa apple.se apple.sg apple.si apple.sk apple.so apple.tt apple.tw apple.uk apple.usapple.xn--czr694bapple.xn--fiqs8s  apple.xyz aplestore.comapple-store.cn -cnapple-store.netapple-store.wang applestor.com applestore.bg applestore.cc applestore.ch applestore.cm applestore.cn -cnapplestore.co.huapplestore.co.jpapplestore.co.ugapplestore.co.ukapplestore.comapplestore.com.auapplestore.com.bnapplestore.com.cn -cnapplestore.com.eeapplestore.com.egapplestore.com.grapplestore.com.hkapplestore.com.hrapplestore.com.joapplestore.com.myapplestore.com.phapplestore.com.plapplestore.com.ptapplestore.com.roapplestore.com.ruapplestore.com.snapplestore.com.tw applestore.de applestore.hk applestore.kr applestore.meapplestore.netapplestore.net.gr applestore.ph applestore.qa applestore.sgapplestore.wangapplestoreonline.comapplestorepro.euonlineapplestore.comapp-store.wangappe-store.comapple-appstore.cn -cnappleappstore.cn -cnappleappstore.netappleappstore.tv  appsto.reappstore.co.id appstore.hk appstore.my appstore.phappstoreapple.cn -cn asto.retvappstore.netapple-imac.com appleimac.comimac-apple.comimac-applecomputer.com imac.co -imac.co.nz imac.com imac.eu imac.gr imac.one imac.rs imacapple.comimacapplecomputer.comebookforipad.com euroipad.comfindmyipad.com indiaipad.com -ipad.co.kr ipad.de  ipad.host  ipad.wang  ipad3.com -ipadair.cl -ipadair.cmipadair.com.bripadair.com.es -ipadair.fr -ipadair.hk -ipadair.ie -ipadair.jp -ipadair.twipadaustralia.com ipadmini.cmipadmini.com.lk ipadmini.lk ipadpro.buzz -ukipad.comappleiphone.huappleiphone.netappleiphonecell.combestiphonestuff.comcooliphonecasesstore.comcustom-iphonecase.comdesigneriphonescases.com dl-iphone.comfundaiphone5s.com hebiphone.com hf-iphone.com -iphine.comiphone-8.com.cn -cn iphone-cd.com iphone-cn.com iphone-sh.comiphone-vip1.comiphone-vip2.comiphone-vip3.comiphone-vip4.comiphone-vip5.com iphone-x.tv iphone-yh.com iphone-zh.com  iphone.cm -iphone.com iphone.com.au iphone.com.gr  iphone.es iphone.host iphone.net.gr  iphone.pt  iphone.rs  iphone.ru iphone.wang iphone4.comiphone4.com.br iphone4s.com iphone5.comiphone5casescovers.comiphone5s5case.comiphoneacessorios.com.briphonecase2013.comiphonecase5.comiphonecases100.comiphonecases5.comiphoneclaro.com.briphonecollcase.com iphonefc.comiphonegermany.comiphonehangzhou.comiphoneimessage.comiphoneplus.wang iphonerip.net iphonese.tviphoneunlockimei.com iphonexs.tvonlyiphone5case.comaplleipods.comappleclassicipod.comdownloadsforipod.com ipod.ca ipod.ch ipod.cm ipod.co -ipod.co.nz -ipod.co.uk -ipod.co.za ipod.com ipod.com.au ipod.com.cn -cn ipod.com.fr ipod.com.hk ipod.com.sg ipod.com.tw ipod.de ipod.es ipod.eu ipod.fr ipod.gr ipod.hk ipod.is ipod.net ipod.no ipod.pk ipod.rs ipod.ru ipod.tw ipodcentre.nlipodcleaner.comipoditouch.com ipodnano.com ipodnano.netipodprices.com -ipodrip.caipodrocks.com.au  ipods.comipodshop.com.au ipodtouch.co ipodtouch.com -myipod.netoffrezdesipods.comsimplyipod.comwwwipodlounge.comimacsources.comlaunchpad.wang mac.wang -macbook.co -macbook.hk -macbook.tw macbook.wang macbookair.cn -cnmacbookair.co.krmacbookair.co.ukmacbookair.commacbookair.com.aumacbookair.com.cn -cnmacbookair.com.es macbookair.es macbookair.hk macbookair.kr macbookair.jpmacbookair.net macbookpro.comacbookpro.commacbookpro.com.aumacbookpro.net macbookpro.usmacbookpros.commacbooksale.comwwwmacbookair.comapple-watch.com.ru applewatch.hk applewatch.tv applewatch.twapplewatch.wangapplewatchedition.comapplewatchseries3.netapplewatchsport.comapplemusic.berlinapplemusic.comapplemusic.com.auapplemusic.hamburgapplemusic.wangapplemusicconnect.comapplemusicfestival.com musickit.netwwwapplemusic.comapple.comscoreresearch.com -apple.newsappleenews.comapplenews.berlinapplenews.hamburg applenews.tvapplenewsformat.com apple-pay.com apple-pay.rsapple-pay.wangapplepay.berlinapplepay.co.rsapplepay.com.twapplepay.hamburg applepay.hk applepay.info applepay.jp applepay.rs applepay.tvapplepaycash.cn -cnapplepaycash.com.cn -cnapplepaycash.netapplepaycash.tv!applepaymerchantsupplies.infoapplepaysupplies.berlinapplepaysupplies.cn -cnapplepaysupplies.com#applepaysupplies.com.cn -cnapplepaysupplies.infoapplepaysupplies.netapplepaysupplies.tvapplewallet.comapplewallet.tv -i-book.com -i-book.net ibook.co.nz  ibook.com ibook.eu  ibook.netibookpartner.comibooksauthor.com  iphoto.eu  iphoto.no  iphoto.se iphoto.wang  imovie.euimoviegallery.comimoviestage.comapplecare.berlin applecare.cc applecare.euapplecare.hamburgapplecare.wangappledarwin.comappledarwin.netdarwinsource.comdarwinsource.orgdarwinsourcecode.comappleaccount.netappleid-applemx.comappleid-applemx.usappleid-iclou.com appleid-uk.usappleid.berlin appleid.comappleid.hamburg -appleid.hk ids-apple.com myappleid.com appletv.com -appletv.fr appletv.wang appletv4.cn -cnappletv4.com.cn -cnappletvapp.appleappleone.audio appleone.blog appleone.chatappleone.cloud appleone.clubappleone.community appleone.filmappleone.guide appleone.hostappleone.space appleone.techappleone.website -shazam.com1to1computing.com.au1to1conference.com.au 5lml.com -a0pple.net aapl.twairport.brussels airport.com -airport.euairsupportapp.com airtunes.com airtunes.info airtunes.netalchemysynth.com  ap0le.com aple.com  apole.com -app0le.com -appl-e.com appl.com -appl4e.com  appla.comapple-darwin.comapple-darwin.netapple-darwin.org apple-dns.cn -cn apple-dns.comapple-dns.com.cn -cn apple-dns.netapple-enews.comapple-expo.com apple-expo.eu apple-hk.comapple-ibooks.cn -cn apple-inc.netapple-livephotoskit.com apple-maps.cn -cnapple-online.com apple-usa.netappleafrica.comappleantilles.comappleaustralia.comappleaustralia.com.auappleaustralia.net.au applebk.netapplecarbon.com applecard.tvapplecentar.co.rsapplecentar.rsapplecenter.cn -cnapplecenter.com.cn -cnapplecentre.com.auapplecentre.infoappleclub.com.hk applecom.comapplecomputer-imac.comapplecomputer.cn -cnapplecomputer.co.inapplecomputer.co.nzapplecomputer.com applecomputer.com.cn -cnapplecomputer.com.hkapplecomputer.com.twapplecomputer.huapplecomputer.krapplecomputerimac.comapplecomputerinc.infoapplecomputers.co.nzapplecoronavirus.com appledns.cn -cnappledns.com.cn -cn appleexpo.euappleexpo.infoapplefilmaker.comapplefilmmaker.comapplefinalcutproworld.comapplefinalcutproworld.netapplefinalcutproworld.orgapplehongkong.comapplehongkong.com.hkappleinclegal.comappleipodsettlement.comappleiservices.com applelink.comapplemagickeyboard.comapplemalaysia.com.myapplemasters.infoappleonline.comappleonline.net appleoriginalproductions.com -appleos.tvapplepencil.netapplepodcasts.comapplepremiumreseller.com.au applepremiumresellers.com.auapplereach.comapplereach.net apples-msk.ruapplescreensavers.comapplescript.infoappleshare.infoappleshop.co.uk applesiri.cn -cnapplesurveys.comappletaiwan.com appletips.net appleweb.net applexpo.net applezh.com  applf.com -applfe.com -applle.com  applw.comappmediagroup.com -appule.com  appye.com  apyle.com  beddit.tvbraeburncapital.com burstly.netburstlyrewards.com buyaapl.com buyaapl.net buyaple.com camelphat.comcamelspaceeffect.com -carbon.com catalina.hk cdn-apple.com  chomp.com -coreml.netdashwood360.comdesktopmovie.comdesktopmovie.netdesktopmovie.orgdesktopmovies.netdesktopmovies.orgdeveloper-advisor.comdevice-manager.usdigitalhub.comdiscussionsapple.com -dot-mac.de  dotmac.dedvdstudiopro.bizdvdstudiopro.comdvdstudiopro.infodvdstudiopro.netdvdstudiopro.orgdvdstudiopro.us -earpod.net ecgapp.com.cn -cn -ecgapp.netedu-research.org -emac.co.in emac.in  emagic.de -eworld.com faceid99.com faceid99.netfaceidglobal.com faceshift.cn -cn facetime.netfinalcutpro.comfind-apple.com firewire.cl firewire.eu geoport.com -homepod.cn -cn -hopstop.tv ichat.co.in idvd.eu  ikids.comilecture.co.nz ilife.eu ilife.gr -ilife.wang imessage.tv insidear.cn -cninsidemacintosh.comiosinthecar.comipa-iphone.net  ipple.com ischool.com itools.info  iwork.com iwork.se -iwork.wangjetfuelapp.comjetfuelapps.comlatticedata.com livephotos.cn -cnlivephotos.com.cn -cn livephotos.tvlojaiphone.com.br mac-mini.com mac.com -mac.com.au -mac.eu -mac.rs macappsto.re macboxset.com mach-os.com mach-os.net -machos.net macintosh.eumacintoshsoftware.com macmini.com macos.com.aumacossierra.com macosx.infomacosxlion.com macpazar.com macreach.com macreach.net mobileme.dkmr-apple.com.tw mzstatic.com -newton.com next.com overapple.com powerbook.eupullstring.netquatrowireless.com quicktime.cc quicktime.comquicktime.com.au quicktime.eu quicktime.net quicktime.tvreform-apple.comservices-apple.net siri.com spektral.cc texture.comthinkdifferent.us -touchid.tv touchid.wangtracking-location.comtruedepth3d.com tryrating.comwebobjects.co.uk webobjects.de webobjects.euwebobjects.net wuxiapple.com wwapple.netwww-sms-apple.com wwwapple.com wwwapple.net wwwlapple.comxn--czrs0t4phtr3a.cnxn--fiqs8sxootzz.cn xn--fiqs8sxootzz.xn--hxt814e!xn--gtvq61aiijy0b.xn--hxt814exn--gtvz22d.wangxn--gtvz22d.xn--hxt814exn--hxtr4rozx.xn--czr694b xn--kput3imi374g.xn--hxt814e#xn--ohq11k7pl25iyo8a.cn -cn xn--ruq8a011kt6y.xn--hxt814e, adcdownload.apple.com.akadns.net -cn!adcdownload.apple.com -cn."amp-api-search-edge.apps.apple.com -cn"amp-api.apps.apple.com -cn aod.itunes.apple.com -cn#api-edge.apps.apple.com -cnappldnld.apple.com -cn"appldnld.g.aaplimg.com -cnapps.apple.com -cnapps.mzstatic.com -cn(apptrailers.itunes.apple.com -cn bag.itunes.apple.com -cn'bookkeeper.itunes.apple.com -cnbooks.apple.com -cn!cds-cdn.v.aaplimg.com -cn$cds.apple.com.akadns.net -cn cds.apple.com -cncl2-cn.apple.com -cn<0cl2.apple.com.edgekey.net.globalredir.akadns.net -cncl4-cn.apple.com -cn'client-api.itunes.apple.com -cn+clientflow.apple.com.akadns.net -cn clientflow.apple.com -cn cma.itunes.apple.com -cn."configuration.apple.com.akadns.net -cn#configuration.apple.com -cn!communities.apple.com -cncstat.apple.com -cn."dd-cdn.origin-apple.com.akadns.net -cn(discussionschinese.apple.com -cn(download.developer.apple.com -cnfides-pol.apple.com -cngs-loc-cn.apple.com -cngs-loc.apple.com -cn%gsp10-ssl-cn.ls.apple.com -cn!gsp11-cn.ls.apple.com -cn!gsp12-cn.ls.apple.com -cn!gsp13-cn.ls.apple.com -cnC7gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net -cn, gsp4-cn.ls.apple.com.edgekey.net -cn gsp4-cn.ls.apple.com -cn gsp5-cn.ls.apple.com -cn(gspe11-2-cn-ssl.ls.apple.com -cn&gspe12-cn-ssl.ls.apple.com -cn&gspe19-cn-ssl.ls.apple.com -cn-!gspe19-cn.ls-apple.com.akadns.net -cn"gspe19-cn.ls.apple.com -cn#gspe21-ssl.ls.apple.com -cngspe21.ls.apple.com -cn#gspe35-ssl.ls.apple.com -cn&gspe85-cn-ssl.ls.apple.com -cniadsdk.apple.com -cn, icloud-cdn.icloud.com.akadns.net -cn icloud.cdn-apple.com -cn'images.apple.com.akadns.net -cn?3images.apple.com.edgekey.net.globalredir.akadns.net -cnimages.apple.com -cn3'init-p01md-lb.push-apple.com.akadns.net -cn init-p01md.apple.com -cn3'init-p01st-lb.push-apple.com.akadns.net -cn%init-p01st.push.apple.com -cn3'init-s01st-lb.push-apple.com.akadns.net -cn%init-s01st.push.apple.com -cn!init.itunes.apple.com -cn$iosapps.itunes.apple.com -cn(iosapps.itunes.g.aaplimg.com -cniphone-ld.apple.com -cn'itunes-apple.com.akadns.net -cnitunes.apple.com -cn#itunesconnect.apple.com -cn"js-cdn.music.apple.com -cn km.support.apple.com -cnmaps.apple.com -cn)mesu-cdn.apple.com.akadns.net -cn+mesu-china.apple.com.akadns.net -cnmesu.apple.com -cnmusic.apple.com -cn(ocsp-lb.apple.com.akadns.net -cnocsp.apple.com -cnoscdn.apple.com -cn-!oscdn.origin-apple.com.akadns.net -cn$osxapps.itunes.apple.com -cnpancake.apple.com -cn, pancake.cdn-apple.com.akadns.net -cnpd.itunes.apple.com -cnphobos.apple.com -cn!play.itunes.apple.com -cn1%prod-support.apple-support.akadns.net -cn$se-edge.itunes.apple.com -cn se2.itunes.apple.com -cn#search.itunes.apple.com -cn1%sf-api-token-service.itunes.apple.com -cnsp.itunes.apple.com -cn4(stocks-sparkline-lb.apple.com.akadns.net -cn&stocks-sparkline.apple.com -cn>2store.apple.com.edgekey.net.globalredir.akadns.net -cn'store.apple.com.edgekey.net -cnstore.apple.com -cn2&store.storeimages.apple.com.akadns.net -cn+store.storeimages.cdn-apple.com -cn+streamingaudio.itunes.apple.com -cnsu.itunes.apple.com -cn2&support-china.apple-support.akadns.net -cnsupport.apple.com -cn."swcatalog-cdn.apple.com.akadns.net -cnswcatalog.apple.com -cnswcdn.apple.com -cnswcdn.g.aaplimg.com -cn'swdist.apple.com.akadns.net -cnswdist.apple.com -cn+swscan-cdn.apple.com.akadns.net -cnswscan.apple.com -cn!sync.itunes.apple.com -cn1%updates-http.cdn-apple.com.akadns.net -cn&updates-http.cdn-apple.com -cn upp.itunes.apple.com -cnvalid.apple.com -cn-!valid.origin-apple.com.akadns.net -cn"weather-data.apple.com -cn<0www.apple.com.edgekey.net.globalredir.akadns.net -cn%www.apple.com.edgekey.net -cn www.apple.com -cn#^a[1-5]\.mzstatic\.com$ -cn0$^cdn(-cn)?[1-4]?\.apple-mapkit\.com$ -cn9-^cl[1-5]-cdn\.origin-apple\.com\.akadns\.net$ -cn!^cl[1-5]\.apple\.com$ -cn+^is[1-5](-ssl)?\.mzstatic\.com$ -cn$^s[1-5]?\.mzstatic\.com$ -cnapple.com.akadns.net%!courier-push-apple.com.akadns.netpush-apple.com.akadns.net asus.com asus.com.cn -cn atlassian.com bitbucket.org statuspage.ioaccbusiness.com -atandt.comatt-bundles.com att-japan.com att-mail.comatt-promotions.com att-rsvp.comatt.cn -cn att.com att.jobs att.net -att.tvattalascom.comattbelieves.comattbusiness.net attcenter.comattcollaborate.com -attcorp.cn -cnattexperts.com attglobal.netattinnovationspace.comattinternetservice.com -attjoy.comattnetclient.comattonlineoffers.com attproxy.comattpublicpolicy.comattpurchasing.comattsavings.comattspecial.com -attssl.comattstadium.comattsuppliers.com atttvnow.comattuverseoffers.comattuverseonline.com attvoip.com attwifi.comattwirelessinternet.comattwirelessonline.comattwirelesssolutions.com bellsouth.net -budatt.com -cobatt.com currently.com currently.netdirectvbusinessmarket.comenterprisepaging.com itcanwait.com mymmode.comnaturalvoices.comnettyinternet.comopenapiplatform.comopenapiservice.compatentgold.netpaygonline.comraponlinereview.com signalbar.comsundayready.com synaptic.netthanksloyalty.com tvpromise.com -uverse.comwebhosting.com att-idns.net -attdns.com -attdns.netna-att-idns.netaudiencenetwork.comaudiencenetwork.tvcabletvdirectv.com dierectv.com -dirctv.com direcpath.com direcpath.netdirecttv-deals.tvdirecttv-dish.com dkrecttv.com directtv.netdirecttvdeals.tvdirecttvreviews.comdirectv-4-you.comdirectv-newyork.com directv.comdirectvadsales.comdirectvatlantaga.comdirectvboston.comdirectvbundles.comdirectvbusiness.comdirectvcincinnatioh.comdirectvcinema.comdirectvconnect.comdirectvcookevilletn.comdirectvcrossvilletn.comdirectvdealer.comdirectvdeals.comdirectvdealsnow.com directvdsl.tvdirectvforhotels.comdirectvgrandslam.comdirectvhouston.comdirectvinternet.comdirectvkentucky.comdirectvlebanontn.comdirectvlosangeles.comdirectvmetropolisil.comdirectvmonitoring.comdirectvmurfreesborotn.comdirectvnewhampshire.comdirectvnow.comdirectvoffercodes.comdirectvonline.comdirectvplans.comdirectvpomise.comdirectvpromise.comdirectvpromotions.comdirectvrebate.comdirectvrichmond.comdirectvsavings.comdirectvsports.comdirectvsundayticket.com direectv.com -diretv.comdtv2009offers.comfreedirecttvspecial.com getdirect.tv hddirectv.com le-direct.tvmydirectgroove.commydirectvchannels.comredzonechannel.com txdirectv.com  xandr.combandwagonhost.com bwh1.net bwh8.net  bwh88.net canon c-ij.com canon-cee.comcanon-cmos-sensors.com canon-cna.comcanon-ebm.com.hkcanon-emea.comcanon-emirates.aecanon-europa.comcanon-europe.com canon-me.com canon-ois.qacanon-se.com.tw canon.am canon.at canon.az canon.ba canon.be canon.bg canon.ca canon.chcanon.cn -cn canon.co.il canon.co.uk canon.co.za  canon.com canon.com.al canon.com.au canon.com.by canon.com.cn -cn canon.com.cy canon.com.hk canon.com.mk canon.com.mt canon.com.my canon.com.tr canon.com.tw canon.cz canon.de canon.dk canon.ee canon.es canon.fi canon.fr canon.ge canon.gr canon.hr canon.hu canon.ie canon.it canon.kz canon.lt canon.lu canon.lv canon.me  canon.net canon.nl canon.no canon.pl canon.pt canon.ro canon.rs canon.ru canon.se canon.si canon.sk canon.tj canon.ua canon.uzcanonfoundation.orgcanonproprinters.com webex.ca webex.co.in webex.co.it webex.co.jp webex.co.kr webex.co.nz webex.co.uk  webex.com webex.com.au webex.com.br webex.com.cn -cn webex.com.hk webex.com.mx webex.de webex.es webex.fr ciscoacademynetriders.comcciernslabs.comcciesecuritylabs.comccievoicelabs.com  ccna5.netcisco-returns.comcisco-warrantyfinder.comcisco.cn -cn  cisco.com cisco.com.cn -cn -cisco.mobiciscoccservice.comciscoconnectcloud.comciscoconnectcloud.netciscoconnectcloud.orgciscocontest.comciscoerate.com ciscofax.comciscoinvestments.comciscojabbervideo.netciscokinetic.comciscoknowledgenetwork.comciscolearningsociety.orgciscolearningsystem.com ciscolive.comcisconetspace.comcisconetspace.infocisconetspace.netcisconetapp.comciscopartnermarketing.comciscopowercube.comciscoprice.comciscoresearch.comciscosoftware.com ciscospark.caciscospark.com ciscospark.jpciscotaccc.com ciscotr.com ciscoturk.netciscovideo.comciscowebseminars.com cloudlock.com cloupia.com cloupia.netconnect-in-canada.comcoreoptics.net cs.co -devm2m.com dnaspaces.io flipshare.comgpstheseries.com inlethd.cominternetofeverything.com ioe.com kscisco.com m2m.com -mcisco.commediafiles-cisco.com metacloud.com mindmeld.commultiplydiversity.commyciscobenefits.com  mysdn.com -mysdn.info  mysdn.net netacad.com netacad.netnetwork-auth.cn -cnobservable.net obsrvbl.com parstream.com parstream.net parstream.orgservices-exchange.comstatic-cisco.comtandberg-china.com tandberg.com -versly.comvideochampion.com virtuata.comwballiance.com!whyiwantciscotelepresence.com wifi-mx.comyoutubecisco.comapp-dynamics.comappdynamics.co.ukappdynamics.comappdynamics.deappdynamics.frappdynamics.infoappdynamics.jpappdynamics.org -mearki.com meraki-go.com  meraki.hk merakigo.com -tail-f.com  tailf.comcisco.evergage.comciscolivehls-i.akamaihd.net clco.cc -cloudc.one cloudcone.com cloudcone.net -cfblob.comcloudchoose.comcloudvolumes.com -cmpaas.com -cpedge.comcrit-staging.comexperience-vmware.com gemfire.netgeodesummit.com getboxer.com gitstar.com gitstar.net gopivotal.net greenplum.net hwslabs.com immidio.comitfromtheinside.comlatamvmwareforum.compivotalinitiative.comsnapvolumes.com ssdevrd.com vcanedge.com vcloudair.net vfabric.netvidmpreview.com -virsto.com -virsto.net vmglobal.net -vmmark.comvmtestdrive.com vmw.comvmware-cloudmanagement.comvmware-techcenter.comvmware20mosaic.comvmwareausnews.com!vmwarecertificationvideos.comvmwarecloud.comvmwaredemandcenter.comvmwareemeablog.comvmwaregrid.comvmwarehorizon.comvmwareidentity.comvmwarelearning.comvmwarelearningplatform.comvmwarestuff.comvmwareusergroupstore.comvmwareviewpoint.com vmwarevmc.com vmwdemo.com vmwgcomms.comvmwhorizonair.comvmwidm-ads.com -vmwidm.comvmwlabconnect.comvmwlearningplatform.com vmworld.comvmworld2010.comvmworld2013.comvmwsalesrewards.comvmwservices.com -vnware.netwhychoosehorizon.comwhychooseview.comwhychoosevmwareeuc.comwindowsuem.comworkspaceair.com xlstudio.comavinetworks.com -badaas.com bitnami.combitnamiapp.combitnamistudio.comdockerizer.comcloudcredibility.comcloudhealthtech.com cloudlive.comkubeacademy.com kubeapps.com -nyansa.com velocloud.com gopivotal.com heptio.cn -cn -heptio.com aliveipc.com aliveitsm.comaliveprofiler.com alivercm.comaliverewind.com alivertsm.com alivevm.com alivevmax.com alivevue.com vsphere.com vsphere.net air-watch.comairwatchexpress.comairwatchqa.com -getws1.com getwsone.comwhatisairwatch.comwhatisworkspaceone.comworkspaceone.comvmware.tt.omtrdc.netdelldell-brand.com dell.com dellcdn.com!achat-followers-instagram.com#acheter-followers-instagram.com$ acheterdesfollowersinstagram.com!acheterfollowersinstagram.combookstagram.comcarstagram.comcdninstagram.comchickstagram.com ig.me  igcdn.com igsonar.com igtv.com imstagram.com imtagram.cominstaadder.cominstachecker.cominstafallow.cominstafollower.cominstagainer.com instagda.com instagify.cominstagmania.com instagor.cominstagram-brand.cominstagram-engineering.cominstagram-help.cominstagram-press.cominstagram-press.net instagram.cominstagramci.cominstagramcn.cominstagramdi.cominstagramhashtags.netinstagramhilecim.cominstagramhilesi.orginstagramium.cominstagramizlenme.cominstagramkusu.cominstagramlogin.cominstagramm.cominstagramn.cominstagrampartners.cominstagramphoto.cominstagramq.cominstagramsepeti.cominstagramtakipcisatinal.netinstagramtakiphilesi.cominstagramtips.cominstagramtr.com instagran.cominstagranm.com instagrem.com instagrm.cominstagtram.com instagy.cominstamgram.cominstangram.cominstanttelegram.cominstaplayer.net instastyle.tv instgram.com intagram.com intagrm.com intgram.comkingstagram.comlnstagram-help.comtheinstagramhack.comoninstagram.comonline-instagram.comonlineinstagram.comweb-instagram.netwwwinstagram.comfbmessenger.comm.me messenger.com -nbabot.net binoculus.com buyoculus.com ocul.usoculus-china.com -oculus.comoculus2014.com oculus3d.comoculusblog.comoculusbrand.comoculuscasino.net oculuscdn.comoculusconnect.comoculusdiving.comoculusforbusiness.comoculusrift.com oculusvr.compowersunitedvr.com wa.mewhatsapp-plus.infowhatsapp-plus.mewhatsapp-plus.net whatsapp.cc whatsapp.com whatsapp.info whatsapp.net whatsapp.org whatsapp.tvwhatsappbrand.comaboutfacebook.com accessfacebookfromschool.com acebooik.com acebook.comadvancediddetection.comaskfacebook.netaskfacebook.org -atdmt2.com atlasdmt.comatlasonepoint.combuyingfacebooklikes.comcareersatfb.comcelebgramme.comchina-facebook.com click-url.comcomo-hackearfacebook.comcrowdtangle.com dacebook.comdlfacebook.comdotfacebook.comdotfacebook.netexpresswifi.com faacebok.com faacebook.com faasbook.com facbebook.com facbeok.com -facboo.com facbook.com facbool.com facboox.com faccebook.comfaccebookk.com facdbook.com facdebook.com face-book.com faceabook.com facebboc.com facebbook.comfacebboook.com facebcook.com facebdok.com facebgook.com facebhook.com facebkkk.com facebo-ok.com faceboak.com facebock.com facebocke.com facebof.com faceboik.com facebok.comfacebokbook.com facebokc.com facebokk.com facebokok.com faceboks.com facebol.com facebolk.com facebomok.com faceboo.com facebooa.com faceboob.comfaceboobok.com facebooc.com faceboock.com facebood.com facebooe.com faceboof.com facebooi.com facebooik.com facebooik.org facebooj.comfacebook-corp.comfacebook-covid-19.comfacebook-ebook.comfacebook-forum.comfacebook-inc.comfacebook-login.comfacebook-newsroom.comfacebook-newsroom.orgfacebook-pmdcenter.comfacebook-pmdcenter.netfacebook-pmdcenter.orgfacebook-privacy.comfacebook-program.comfacebook-studio.comfacebook-support.orgfacebook-texas-holdem.comfacebook-texas-holdem.net facebook.br facebook.ca facebook.cc facebook.comfacebook.design facebook.net facebook.hu facebook.in fecebook.net facebook.nl facebook.org facebook.se facebook.shop facebook.tv facebook.us facebook.wangfacebook123.orgfacebook30.comfacebook30.netfacebook30.orgfacebook4business.comfacebookads.com"facebookadvertisingsecrets.comfacebookatschool.comfacebookawards.comfacebookblueprint.netfacebookbrand.comfacebookbrand.net3/facebookcanadianelectionintegrityinitiative.comfacebookcareer.comfacebookcheats.comfacebookck.comfacebookclub.comfacebookcom.comfacebookconsultant.orgfacebookcoronavirus.comfacebookcovers.orgfacebookcredits.infofacebookdating.netfacebookdusexe.orgfacebookemail.comfacebookenespanol.comfacebookexchange.comfacebookexchange.netfacebookfacebook.comfacebookflow.comfacebookgames.comfacebookgraphsearch.comfacebookgraphsearch.infofacebookgroups.comfacebookhome.ccfacebookhome.comfacebookhome.infofacebookhub.com facebooki.comfacebookinc.comfacebookland.comfacebooklikeexchange.comfacebooklive.comfacebooklivestaging.netfacebooklivestaging.orgfacebooklogin.comfacebooklogin.infofacebookloginhelp.netfacebooklogs.comfacebookmail.comfacebookmail.tvfacebookmanager.infofacebookmarketing.info facebookmarketingpartner.com!facebookmarketingpartners.comfacebookmobile.comfacebookmsn.comfacebooknews.comfacebooknfl.comfacebooknude.comfacebookofsex.comfacebookook.comfacebookpaper.comfacebookpay.comfacebookphonenumber.netfacebookphoto.comfacebookphotos.comfacebookpmdcenter.comfacebookpoke.netfacebookpoke.orgfacebookpoker.infofacebookpokerchips.infofacebookporn.netfacebookporn.orgfacebookporno.netfacebookportal.com facebooks.comfacebooksafety.comfacebooksecurity.netfacebookshop.comfacebooksignup.netfacebooksite.netfacebookstories.comfacebookstudios.netfacebookstudios.orgfacebooksupplier.comfacebooksuppliers.comfacebookswagemea.comfacebookswagstore.comfacebooksz.comfacebookthreads.netfacebooktv.netfacebooktv.orgfacebookvacation.com facebookw.comfacebookworld.com facebool.com facebool.info facebooll.com faceboom.com faceboon.com faceboonk.comfaceboooik.com faceboook.com faceboop.com faceboot.com faceboox.com facebopk.com facebpook.com facebuk.com facebuok.com facebvook.com facebyook.com facebzook.comfacecbgook.com facecbook.com facecbook.org facecook.com facecook.org facedbook.com faceebok.com faceebook.com faceebot.com facegbok.com facegbook.com faceobk.com faceobok.com faceobook.com faceook.comfacerbooik.com facerbook.com facesbooc.comfacesounds.com facetook.com facevbook.com facewbook.co facewook.comfacfacebook.com facfebook.com fackebook.com facnbook.com facrbook.com facvebook.com facwebook.com facxebook.com fadebook.com -faebok.com faebook.com faebookc.com faeboook.com faecebok.com faesebook.comfafacebook.com faicbooc.com fasebokk.com fasebook.com faseboox.com favebook.com faycbok.com -fb.careers -fb.com fb.me fbacebook.com fbbmarket.comfbboostyourbusiness.com  fbcdn.com  fbcdn.netfbfeedback.com -fbhome.com fbidb.io  fbinc.comfbinnovation.comfbmarketing.com  fbreg.com -fbrpms.com  fbsbx.com  fbsbx.netfbsupport-covid.netfbthirdpartypixel.comfbthirdpartypixel.netfbthirdpartypixel.org  fburl.comfbworkmail.com fcacebook.com fcaebook.com fcebook.com fcebookk.comfcfacebook.comfdacebook.info feacboo.com feacbook.com feacbooke.com feacebook.com fecbbok.com fecbooc.com fecbook.com feceboock.com feceboox.com fececbook.com  feook.com ferabook.com fescebook.com fesebook.com ffacebook.com fgacebook.com ficeboock.com fmcebook.com fnacebook.com fosebook.com fpacebook.com fqcebook.com fracebook.com  freeb.comfreebasics.comfreebasics.net -freebs.comfreefacebook.comfreefacebook.netfreefacebookads.netfreefblikes.comfreindfeed.comfriendbook.info friendfed.comfriendfeed-api.comfriendfeed-media.comfriendfeed.comfriendfeedmedia.com fsacebok.com fscebook.comfundraisingwithfacebook.comfunnyfacebook.orgfutureofbusinesssurvey.org gacebook.com gameroom.comgfacecbook.com -groups.comhackerfacebook.comhackfacebook.comhackfacebookid.comhifacebook.info!howtohackfacebook-account.comhsfacebook.comhttpfacebook.comhttpsfacebook.comhttpwwwfacebook.com i.org internet.org klik.me liverail.com liverail.tvlogin-account.netmarkzuckerberg.commidentsolutions.commobilefacebook.commoneywithfacebook.com myfbfans.com newsfeed.com nextstop.comonline-deals.netopencreate.orgreachtheworldonfacebook.com rocksdb.orgshopfacebook.comsportsfacebook.comsportstream.comsupportfacebook.comterragraph.comthefacebook.comthefacebook.net thefind.comtoplayerserver.comviewpointsfromfacebook.comwhyfacebook.com worldhack.comwww-facebook.comwwwfacebok.comwwwfacebook.comwwwmfacebook.comzuckerberg.comzuckerberg.netfacebookwork.com -redkix.com workplace.comworkplaceusecases.comfbcdn-a.akamaihd.net  aorus.com aoruszone.cn -cn gigabyte.cn -cn gigabyte.comgigabyte2.azureedge.netsa78gs.wpc.edgecastcdn.net"sni1dcb6gl.wpc.edgecastcdn.net godaddy.comx.co godaddy blogspot.ae blogspot.al blogspot.am blogspot.ba blogspot.be blogspot.bg blogspot.bj blogspot.ca blogspot.cf blogspot.ch blogspot.clblogspot.co.atblogspot.co.idblogspot.co.ilblogspot.co.keblogspot.co.nzblogspot.co.ukblogspot.co.za blogspot.comblogspot.com.arblogspot.com.aublogspot.com.brblogspot.com.byblogspot.com.coblogspot.com.cyblogspot.com.eeblogspot.com.egblogspot.com.esblogspot.com.mtblogspot.com.ngblogspot.com.trblogspot.com.uy blogspot.cv blogspot.cz blogspot.de blogspot.dk blogspot.fi blogspot.fr blogspot.gr blogspot.hk blogspot.hr blogspot.hu blogspot.ie blogspot.in blogspot.is blogspot.it blogspot.jp blogspot.kr blogspot.li blogspot.lt blogspot.lu blogspot.md blogspot.mk blogspot.mx blogspot.my blogspot.nl blogspot.no blogspot.pe blogspot.pt blogspot.qa blogspot.re blogspot.ro blogspot.rs blogspot.ru blogspot.se blogspot.sg blogspot.si blogspot.sk blogspot.sn blogspot.td blogspot.tw blogspot.ug blogspot.vn2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -ads youtubeggpht.cn -cn  ggpht.comgooglevideo.comwithyoutube.com youtu.beyoutube-nocookie.com -youtube.ae -youtube.al -youtube.am -youtube.at -youtube.az -youtube.ba -youtube.be -youtube.bg -youtube.bh -youtube.bo -youtube.by -youtube.ca youtube.cat -youtube.ch -youtube.cl -youtube.co youtube.co.ae youtube.co.at youtube.co.cr youtube.co.hu youtube.co.id youtube.co.il youtube.co.in youtube.co.jp youtube.co.ke youtube.co.kr youtube.co.ma youtube.co.nz youtube.co.th youtube.co.tz youtube.co.ug youtube.co.uk youtube.co.ve youtube.co.za youtube.co.zw youtube.comyoutube.com.aryoutube.com.auyoutube.com.azyoutube.com.bdyoutube.com.bhyoutube.com.boyoutube.com.bryoutube.com.byyoutube.com.coyoutube.com.doyoutube.com.ecyoutube.com.eeyoutube.com.egyoutube.com.esyoutube.com.ghyoutube.com.gryoutube.com.gtyoutube.com.hkyoutube.com.hnyoutube.com.hryoutube.com.jmyoutube.com.joyoutube.com.kwyoutube.com.lbyoutube.com.lvyoutube.com.lyyoutube.com.mkyoutube.com.mtyoutube.com.mxyoutube.com.myyoutube.com.ngyoutube.com.niyoutube.com.omyoutube.com.payoutube.com.peyoutube.com.phyoutube.com.pkyoutube.com.ptyoutube.com.pyyoutube.com.qayoutube.com.royoutube.com.sayoutube.com.sgyoutube.com.svyoutube.com.tnyoutube.com.tryoutube.com.twyoutube.com.uayoutube.com.uyyoutube.com.ve -youtube.cr -youtube.cz -youtube.de -youtube.dk -youtube.ee -youtube.es -youtube.fi -youtube.fr -youtube.ge -youtube.gr -youtube.gt -youtube.hk -youtube.hr -youtube.hu -youtube.ie -youtube.in -youtube.iq -youtube.is -youtube.it -youtube.jo -youtube.jp -youtube.kr -youtube.kz -youtube.la -youtube.lk -youtube.lt -youtube.lu -youtube.lv -youtube.ly -youtube.ma -youtube.md -youtube.me -youtube.mk -youtube.mn -youtube.mx -youtube.my -youtube.ng -youtube.ni -youtube.nl -youtube.no -youtube.pa -youtube.pe -youtube.ph -youtube.pk -youtube.pl -youtube.pr -youtube.pt -youtube.qa -youtube.ro -youtube.rs -youtube.ru -youtube.sa -youtube.se -youtube.sg -youtube.si -youtube.sk -youtube.sn youtube.soy -youtube.sv -youtube.tn -youtube.tv -youtube.ua -youtube.ug -youtube.uy -youtube.vnyoutubeeducation.comyoutubegaming.comyoutubego.co.idyoutubego.co.in youtubego.comyoutubego.com.br youtubego.id youtubego.inyoutubei.googleapis.comyoutubekids.comyoutubemobilesupport.com yt.be  ytimg.comand -chromedclkgbizgle gmailgoogoog -googleguge hangout nexus xn--qcka1pmc  google.ad  google.ae  google.al  google.am  google.as  google.at  google.az  google.ba  google.be  google.bf  google.bg  google.bi  google.bj  google.bs  google.bt  google.by  google.ca -google.cat  google.cd  google.cf  google.cg  google.ch  google.ci  google.cl  google.cm google.cn -cn google.co.ao google.co.bw google.co.ck google.co.cr google.co.id google.co.il google.co.in google.co.jp google.co.ke google.co.kr google.co.ls google.co.ma google.co.mz google.co.nz google.co.th google.co.tz google.co.ug google.co.uk google.co.uz google.co.ve google.co.vi google.co.za google.co.zm google.co.zw -google.com google.com.af google.com.ag google.com.ai google.com.ar google.com.au google.com.bd google.com.bh google.com.bn google.com.bo google.com.br google.com.bz google.com.co google.com.cu google.com.cy google.com.do google.com.ec google.com.eg google.com.et google.com.fj google.com.gh google.com.gi google.com.gt google.com.hk google.com.jm google.com.kh google.com.kw google.com.lb google.com.ly google.com.mm google.com.mt google.com.mx google.com.my google.com.na google.com.ng google.com.ni google.com.np google.com.om google.com.pa google.com.pe google.com.pg google.com.ph google.com.pk google.com.pr google.com.py google.com.qa google.com.sa google.com.sb google.com.sg google.com.sl google.com.sv google.com.tj google.com.tr google.com.tw google.com.ua google.com.uy google.com.vc google.com.vn  google.cv  google.cz  google.de  google.dj  google.dk  google.dm  google.dz  google.ee  google.es  google.fi  google.fm  google.fr  google.ga  google.ge  google.gg  google.gl  google.gm  google.gr  google.gy  google.hn  google.hr  google.ht  google.hu  google.ie  google.im  google.iq  google.is  google.it  google.je  google.jo  google.kg  google.ki  google.kz  google.la  google.li  google.lk  google.lt  google.lu  google.lv  google.md  google.me  google.mg  google.mk  google.ml  google.mn  google.ms  google.mu  google.mv  google.mw  google.ne  google.nl  google.no  google.nr  google.nu  google.pl  google.pn  google.ps  google.pt  google.ro  google.rs  google.ru  google.rw  google.sc  google.se  google.sh  google.si  google.sk  google.sm  google.sn  google.so  google.sr  google.st  google.td  google.tg  google.tl  google.tm  google.tn  google.to  google.tt  google.vg  google.vu  google.ws 0emm.com  1e100.net  1ucrs.com -466453.com abc.xyz adgoogle.net -admeld.comangulardart.org -api.ai -apigee.com appbridge.ca appbridge.io appbridge.it appspot.com -apture.com area120.com -asp-cc.com autodraw.com bandpage.combaselinestudy.combaselinestudy.org bazel.build bdn.devbeatthatquote.com  blink.org blogblog.com blogger.combrocaproject.com -brotli.org bumpshare.com -bumptop.ca bumptop.com bumptop.net bumptop.org bumptunes.comcampuslondon.com certificate-transparency.org chat.gle -chrome.comchromebook.comchromecast.com chromeos.dev chromium.orgcloudburstresearch.comcloudfunctions.netcloudrobotics.comcobrasearch.com codespot.com conscrypt.com conscrypt.orgcookiechoices.org  coova.com  coova.net  coova.orgcrossmediapanel.com crr.com  cs4hs.comdartsearch.netdataliberation.org  debug.comdebugproject.com deepmind.comdevsitetest.howdialogflow.comdigitalassetlinks.org episodic.comfeedburner.com -fflick.comfinanceleadsonline.comfirebaseapp.comfirebaseio.com -foofle.com froogle.com fuchsia.dev  g-tun.comg.cn -cng.co g.dev -g.pagegerritcodereview.comgetbumptop.com ggoogle.com gipscorp.com gkecnapps.cn -cn globaledu.org  gmail.com gmodules.com  gogle.com -gogole.comgonglchuangl.net -goo.gl -googel.com -googil.com  googl.comgoogle-syndication.com google.berlin -google.dev -google.net -google.orggoogle.ventures"googleacquisitionmigration.com googleapis.cn -cngoogleapis.comgoogleapps.comgooglearth.comgoogleblog.com googlebot.comgooglecapital.comgooglecert.netgooglecnapps.cn -cngooglecode.comgooglecommerce.comgooglecompare.co.ukgoogledanmark.comgoogledomains.comgoogledrive.com googlee.comgoogleearth.comgooglefiber.netgooglefinland.comgooglemail.comgooglemaps.comgooglepagecreator.comgooglephotos.comgoogleplay.comgoogleplus.comgooglesource.comgooglestore.comgooglesverige.comgoogleusercontent.comgoogleventures.com -googlr.com -goolge.com gooogle.com gridaware.app gsrc.io -gstatic.cn -cn gstatic.comgstaticcnapps.cn -cn -gsuite.com -gv.com gvt0.com gvt1.com gvt2.com gvt3.com gvt5.com gvt6.com gvt7.com gvt9.comhdrplusdata.org hey.gle hindiweb.comhowtogetmo.co.ukhtml5rocks.com hwgo.comiamremarkable.org igoogle.com impermium.com -j2objc.orgjibemobile.com keyhole.comkeytransparency.comkeytransparency.fookeytransparency.org lanternal.com like.commadewithcode.com material.io mdialog.com meet.newmfg-inspector.commobileview.pagemoodstocks.com near.by nest.com nomulus.foo -oauthz.com -ok.gle on.here on2.comonefifteen.netonefifteen.orgoneworldmanystories.com openthread.io openweave.io orbitera.com  page.linkpagespeedmobilizer.com pageview.mobi panoramio.compartylikeits1986.orgpaxlicense.org -picasa.com picasaweb.com picasaweb.net picasaweb.org -picnik.com pittpatt.com postini.comprojectara.comprojectbaseline.compublishproxy.comquestvisual.comquickoffice.com quiksee.com -revolv.comridepenguin.com run.appsavethedate.foo -saynow.com schemer.comscreenwisetrends.comscreenwisetrendspanel.com snapseed.com solveforx.com -stadia.devstcroixmosquito.comstcroixmosquitoproject.comstudywatchbyverily.comstudywatchbyverily.orgstxmosquito.comstxmosquitoproject.comstxmosquitoproject.netstxmosquitoproject.org synergyse.comtensorflow.org  tfhub.devthecleversense.comthegooglestore.comthinkquarterly.co.ukthinkquarterly.comthinkwithgoogle.com tiltbrush.com txcloud.net  txvia.comunfiltered.news useplannr.comusvimosquito.comusvimosquitoproject.comvelostrata.com -verily.comverilylifesciences.comverilystudyhub.comverilystudywatch.comverilystudywatch.org -wallet.com  waymo.com waze.com web.app web.devwebappfieldguide.comwebmproject.orgwebpkgcache.com -webrtc.orgweltweitwachsen.dewhatbrowser.org widevine.comwithgoogle.com womenwill.comwomenwill.com.br womenwill.id womenwill.in womenwill.mx  x.company -x.team xn--9kr7l.comxn--9trs65b.comxn--flw351e.com xplr.cozukunftswerkstatt.de265.com -cn2mdn.net -cn adservice.google.com -cnapp-measurement.com -cn beacons.gcp.gvt2.com -cnbeacons.gvt2.com -cnbeacons3.gvt2.com -cn c.admob.com -cn(c.android.clients.google.com -cn!cache.pack.google.com -cncheckin.gstatic.com -cn%clickserve.dartsearch.net -cn)clientservices.googleapis.com -cn)connectivitycheck.gstatic.com -cncorp.google.com -cn crl.pki.goog -cn"csi-china.l.google.com -cncsi.gstatic.com -cn dl.google.com -cndl.l.google.com -cndoubleclick.net -cn-!firebase-settings.crashlytics.com -cn fonts.googleapis.com -cnfonts.gstatic.com -cn google-analytics.com -cn googleadservices.com -cngoogleanalytics.com -cn!googlesyndication.com -cn googletagmanager.com -cn!googletagservices.com -cngtm.oasisfeng.com -cn!imasdk.googleapis.com -cn kh.google.com -cn ocsp.pki.goog -cn, pagead-googlehosted.l.google.com -cn!pki-goog.l.google.com -cn recaptcha.net -cnredirector.gvt1.com -cn)safebrowsing-cache.google.com -cn'safebrowsing.googleapis.com -cn$settings.crashlytics.com -cn-!ssl-google-analytics.l.google.com -cnssl.gstatic.com -cn%toolbarqueries.google.com -cntools.google.com -cntools.l.google.com -cn$translate.googleapis.com -cn!update.googleapis.com -cn-!www-googletagmanager.l.google.com -cnwww.gstatic.com -cnwww.recaptcha.net -cn0$^khm([0-3]|db)?\.google(apis)?\.com$ -cn  hinet.net -123-hp.com123hplaserjet.com8008206616.com as-hp.cabaterias-hp.com campushp.comcolorprotechnology.com -compaq.orgdavid-laserscanner.com demoprint.comdigitalpack.com eprintsw.comgaming-notebooks.comgetprintersupports.comhp-imagine.comhp-infolab.com hp-invent.comhp-invent.info -hp-mns.comhp-printing.comhp-webplatform.com  hp-ww.com -hp.com -hp.company hp.io hp3d.comhp3dmetals.comhp3dprinter.comhp3dsamplepromo.comhpallinoneprinter.comhpapplicationscenter.com hpbestbuy.comhpbluecarpet.comhpbluecarpet.nethpbuiltforlearning.com hpbundle.com hpcampus.comhpcatridge.com  hpccp.com  hpceo.com  hpcmw.nethpcodewarsbcn.comhpcomputers.comhpcomputerservices.comhpconnected.comhpconnected.nethpconnected.orghpconnected.ushpconnectedstage.comhpcontinuum.com  hpcpi.com hpcu.orghpcustomersupport.comhpcustomersupport.net -hpdaas.comhpdesignjetl25500.comhpdesktopcomputer.comhpdownloadstore.comhpdreamcolor.com hpdriver.com hpdrivers.com hpdrivers.org hpeclipse.com hpengage.com hpeprint.comhpeprintcenter.com hpeurope.comhpeventcenter.com -hpfaqs.comhpfeedback.com -hpgift.com -hpgpas.comhpgraphicspartners.com  hpiie.org -hpinc.info  hpinc.nethpindigopress.com hpinkjet.comhpinstantink.cahpinstantink.comhpkeyboard.com hplaptop.comhplaptopbattery.comhplargeformatremote.comhplatexknowledgecenter.com hplfmedia.com  hplip.nethplipopensource.comhpmarketplace.comhpmemorychips.comhpmicrcartridge.com -hpmini.com hpmobile.comhpofficejetprinter.comhponlinehelp.comhponlineprinting.comhppage5000.comhppavillionlaptop.comhpphotoscanners.comhpprinterdrivers.comhpprinterinstalls.comhpprintersupplies.comhpsalescentral.com hpserver.comhpservices.comhpshooping.com -hpshop.com hpshoping.com hpshopping.hk hpsignage.com hpsmart.com hpsmarts.comhpsmartstage.comhpsmartupdate.comhpsoftware.com hpspeaker.comhpsprocket.com  hpsso.comhpstore-china.comhpstore-emea.com -hpstore.cn -cn hpstore.comhpstorethailand.com hpsupport.comhpsuresupply.comhptechventures.com hpto.net hptouch.comhptouchpointmanager.comhptrainingcenter.com  hpuae.comhpuniversity.infohpusertraining.com -hpveer.comhpvirtualthin.com hpwallart.comhpwellnesscentral.com  hpwsn.com i-123-hp.com inkchoice.cominstantink.comleavinghpinc.comlife-global.orgmyhpsupport.com printeron.comprintersetupsupport.comprintspots.comprintspots.netserviceshp.comthecompaqstore.comtouchsmartpc.comtouchsmartpc.nettouchsmartpc.orgworkstations.tv www8-hp.comxn--6eup7j.comxn--6eup7j.nethpstore.corpmerchandise.comibmweatherchannel weather bluemix.com bluemix.net ibm.com -ibm.eu ibm.net -ibm.us s-bluemix.net s81c.com intel intel.ac intel.ae intel.af intel.ag intel.ai intel.ar intel.at intel.az intel.ba intel.bg intel.bh intel.bi intel.bo intel.bs intel.by intel.ca intel.cc intel.cg intel.ch intel.cl intel.cmintel.cn -cn intel.co.ae intel.co.cr intel.co.id intel.co.il intel.co.jp intel.co.kr intel.co.uk intel.co.za  intel.com intel.com.ar intel.com.au intel.com.bo intel.com.br intel.com.cn -cn intel.com.co intel.com.ec intel.com.hk intel.com.jm intel.com.mx intel.com.my intel.com.pe intel.com.ph intel.com.pr intel.com.py intel.com.tr intel.com.tw intel.com.uy intel.com.ve intel.cr intel.cu intel.cz intel.de intel.dk intel.dz intel.ec intel.ee intel.eg intel.es intel.eu intel.fi intel.fr intel.ga intel.gd intel.ge intel.gl intel.gm intel.gr intel.gs intel.gt intel.gy intel.hk intel.hn intel.ht intel.hu intel.ie intel.in intel.io intel.it intel.je intel.jo intel.jp intel.ke intel.la intel.lc intel.lk intel.lt intel.lu intel.ly intel.ma intel.md intel.me intel.mg intel.mk intel.mn intel.mp intel.mt intel.mu intel.mw intel.mx intel.my intel.ng intel.nl intel.nu intel.nz intel.pa intel.pe intel.ph intel.pl intel.pn intel.re intel.ro intel.ru intel.rw intel.sa intel.sc intel.se intel.sg intel.si intel.sk intel.sn intel.sr intel.st intel.sv intel.sx intel.sy intel.tf intel.tj intel.tl intel.tm intel.tn intel.tt intel.tv intel.tw intel.ua intel.uk intel.us intel.uy intel.uz intel.vg intel.vn intel.vu intel.wf intel.yt -altera.comalteraforum.comalteraforums.comalteraforums.netalterauserforum.comalterauserforum.netalterauserforums.comalterauserforums.net buyaltera.com celeron.com celeron.net centrino.com centrino.net  chips.com cilk.com cilk.netcloudinsights.comclusterconnection.com coreduo.comcoreextreme.comcrosswalk-project.comcrosswalk-project.netdoceapower.com  easic.com enpirion.comexascale-tech.comexploreintel.comgordonmoore.cominsidefilms.com intc.comintel-research.net&"intel-university-collaboration.netintelamericasstore.comintelapacstore.com intelatom.netintelcapital.comintelcapital.netintelcloudbuilders.comintelcloudfinder.comintelemeastore.cominteleventexpress.comintelforchange.comintelfreepress.com intelgo.netintelinsight.cominteliotmarketplace.com -intell.comintellearningseries.comintellinuxwireless.netintelnervana.comintelnet.component intelplay.comintelquark.comintelrealsense.cn -cnintelrealsense.com intelrxt.comintelsalestraining.comintelsecurity.comintelserveredge.comintelsoftwarenetwork.comintelstore.cominteltechnologyprovider.com intelvmwarecybersecurity.com  itnel.comlatencytop.comlookinside.commakebettercode.commakesenseofdata.com movidius.com movidius.netnervanasys.com  nevex.comnextgenerationcenter.com -niosii.com -niosii.netomekinteractive.com -omnitek.tv openamt.comopendroneid.orgoptanedifference.com -pc.com pentium.com pentium.net pintool.compoweredbyintel.comreconinstruments.com reconjet.comresearchintel.comsaffrontech.comsensorynetworks.com -siport.comsmart-edge.comtheintelstore.comthunderbolttechnology.nettrustedanalytics.comtrustedanalytics.net -vokevr.com vpro.com vpro.net xeon.comxn--ztsq84g.cn -xscale.combarefootnetworks.com linkedin.com  licdn.com logi.comlogicool.co.jp logicoolg.com logitech.biz logitech.com logitech.fr logitechg.com logitechg.frworldsfastestgamer.net azure-dns.comazure-dns.info azure-dns.net azure-dns.orgazure-mobile.netazure-sphere.comazure-test.netazure.cn -cn  azure.com  azure.netazurecomcdn.netazurecontainer.ioazurecosmos.netazurecosmosdb.comazurecosmosdb.infoazurecosmosdb.netazuredatabricks.netazuredevopslaunch.comazuredigitaltwin.comazuredigitaltwins.comazuredigitaltwins.netazuredns-prd.infoazuredns-prd.orgazureedge-test.net azureedge.netazureiotcentral.comazureiotsolutions.comazureiotsuite.comazureplanetscale.infoazureplanetscale.netazureserviceprofiler.comazuresmartspaces.netazurestackvalidation.comazurewebsites.netchinacloudapi.cn -cnchinacloudapp.cn -cnchinacloudsites.cn -cn cosmosdb.info cosmosdb.net devopsms.com gotcosmos.commicrosofteca.commicrosoftiotcentral.comserverlesslibrary.nettryfunctions.comwindowsazure.cn -cnwindowsazure.comazure.microsoft.com"azuremarketplace.microsoft.combing bing.com bing.com.cn -cn bing.net bingads.combingagencyawards.combingapistatistics.combingsandbox.combingvisualsearch.com bingworld.combluehatnights.com -dictate.msflipwithsurface.commasalladeloslimites.commicrosoft-give.commicrosoftcloudsummit.commicrosoftdiplomados.commicrosoftlatamholiday.commicrosoftmxfilantropia.com!microsoftpartnersolutions.commsunlimitedcloudsummit.comoffice365love.comoffice365tw.comrenovacionoffice.comsprinklesapp.com cn.bing.com -cn cn.bing.net -cn ditu.live.com -cnbj1.api.bing.com -cnemoi-cncdn.bing.com -cnmicrosoftnews.ccmicrosoftnews.cn -cnmicrosoftnews.commicrosoftnews.netmicrosoftnews.orgmicrosoftnewsforkids.commicrosoftnewsforkids.netmicrosoftnewsforkids.orgmicrosoftnewskids.commicrosoftnewskids.netmicrosoftnewskids.org msn.commsnewskids.commsnewskids.netmsnewskids.org msnkids.com 1drv.com 1drv.mslivefilestore.com onedrive.coonedrive.co.uk onedrive.com onedrive.eu onedrive.net onedrive.orgonedrive.live.comstorage.live.com  microsoft azure hotmail -office skype windowsxbox microsoft.az microsoft.be microsoft.by microsoft.ca microsoft.cat microsoft.ch microsoft.cl microsoft.com microsoft.cz microsoft.de microsoft.dk microsoft.ee microsoft.es microsoft.eu microsoft.fi microsoft.ge microsoft.hu microsoft.io microsoft.is microsoft.it microsoft.jp microsoft.lt microsoft.lu microsoft.lv microsoft.md microsoft.pl microsoft.pt microsoft.red microsoft.ro microsoft.rs microsoft.ru microsoft.se microsoft.si microsoft.tv microsoft.ua microsoft.uz microsoft.vnfemalefounderscomp.com -m12.vc -aka.msankarazirvesi2018.comapplicationinsights.ioapplicationinsights.net aria.ms asp.net aspnetcdn.com -binads.com bluehatil.combrazilpartneruniversity.com breakdown.mecentralvalidation.com -ch9.mscharticulator.com cloudapp.netcloudappsecurity.comcortanaanalytics.comcortanaskills.comcrmdynint-gcc.com crmdynint.comcrossborderexpansion.com docs.com dynamics.com efproject.netazuredigitaltwin.comfasttrackreadysupport.comfluidpreview.comgameuxmasterguide.com -gears5.com gearspop.comgearstactics.com -gfx.ms -gigjam.com hololens.com -hotmail.co hotmail.com -hotmail.eu hotmail.net hotmail.orghummingbird.ms -ie8.co ie9.com ie10.com ie11.cominternetexplorer.cointernetexplorer.com imaginecup.pl -ingads.comintunewiki.comiotinactionevents.com -kidgrid.tvlatampartneruniversity.com live.com live.com.au live.eu live.netlivingyourambition.com makecode.orgmanagedmeetingrooms.commeetfasttrack.commeetyourdevices.com mepn.commicrosoft-int.commicrosoft-ppe.commicrosoft-sap-events.commicrosoft-sbs-domains.commicrosoft-smb.cn -cnmicrosoft365.commicrosoftaccountguard.commicrosoftads.commicrosoftadvertising.com*µsoftadvertisingregionalawards.commicrosoftaffiliates.commicrosoftcloud.commicrosoftcloudworkshop.com"microsoftcommunitytraining.commicrosoftedge.commicrosofthouse.commicrosofthouse.netmicrosoftinternetsafety.netmicrosoftlatamaitour.commicrosoftlinc.commicrosoftonline-p.commicrosoftonline.com!microsoftpartnercommunity.commicrosoftready.commicrosoftsiteselection.commicrosoftstore.commicrosoftstore.com.hkmicrosoftstream.commicrosoftteams.commicrosofttradein.commicrosofttranslator.commicrosoftuwp.commomentumms.commorphcharts.commpnevolution.comms-studiosmedia.comms365surfaceoffer.com  msads.net msauth.cn -cn -msauth.netmschallenge2018.com -msecnd.net -msedge.net  msft.info msft.net msftauth.cn -cn msftauth.netmsfteducation.ca msftnet.orgmsgamesresearch.com -msocdn.com -msocsp.com msturing.orgmsudalosti.commymicrosoft.comnextechafrica.net nxta.orgo365weve-dev.como365weve-ppe.com o365weve.com -office.com -office.net office365.com onenote.com onenote.net onestore.msopticsforthecloud.netoutingsapp.com outlook.com passport.net -pixapp.netpowerappscdn.netpowerautomate.com powerbi.compowershellgallery.comprojectmurphy.netprojectsangam.com -pxt.ios-microsoft.com  s-msn.com sfbassets.com sfbassets.net -sfx.ms sharepoint.cn -cnsharepoint.comsharepointonline.com  skype.com  skype.netskypeassets.comskypeassets.net staffhub.mssuccesswithteams.com surface.com sway-cdn.com sway.comsysinternals.comtailwindtraders.com -techhub.ms tfsallin.nettrafficmanager.nettranslatetheweb.comunlocklimitlesslearning.com -userpxt.iovideobreakdown.comvideoindexer.aivirtualearth.net vsallin.net -wbd.ms what-fan.netwindows-int.net windows.com windows.net -windows.nl windows8.hkwindowscommunity.netwindowsmarketplace.comwindowsphone-int.comwindowsphone.comwindowssearch.comwindowsupdate.com -winhec.com -winhec.net -yammer.comcdn.office.net -cn(dl.delivery.mp.microsoft.com -cn&download.windowsupdate.com -cn'bg.v4.a.dl.ws.microsoft.com -cn(bg4.v4.a.dl.ws.microsoft.com -cn2&cdn.marketplaceimages.windowsphone.com -cn cn.windowssearch.com -cn#ctldl.windowsupdate.com -cn#developer.microsoft.com -cndocs.microsoft.com -cn"download.microsoft.com -cn/#download.visualstudio.microsoft.com -cnfs.microsoft.com -cn(msgr.dlservice.microsoft.com -cn, msgruser.dlservice.microsoft.com -cn#officecdn.microsoft.com -cn&qh.dlservice.microsoft.com -cnsdx.microsoft.com -cn/#silverlight.dlservice.microsoft.com -cn, slupdate.dlservice.microsoft.com -cn, storeedgefd.dsx.mp.microsoft.com -cn&wl.dlservice.microsoft.com -cn&wscont1.apps.microsoft.com -cn&wscont2.apps.microsoft.com -cnwww.microsoft.com -cn&xb.dlservice.microsoft.com -cn/+img-prod-cms-rt-microsoft-com.akamaized.netimg-s-msn-com.akamaized.netmwf-service.akamaized.net)%officecdn-microsoft-com.akamaized.net3/statics-marketingsites-eus-ms-com.akamaized.net40statics-marketingsites-wcus-ms-com.akamaized.net firefox.comdeveloper.mozilla.org($interactive-examples.mdn.mozilla.netmdn.mozillademos.orgmedia.prod.mdn.mozit.cloud rust-lang.org  rustup.rs  crates.io docs.rsmozilla.community mozilla.net mozilla.org bugzilla.org getpocket.comseamonkey-project.orgthunderbird.net grafolio.com  naver.com naver.jp  naver.net navercorp.com  plug.game pstatic.net neuralink.com -geforce.cn -cn geforce.co.kr geforce.co.uk geforce.comgeforce.com.twgputechconf.cn -cngputechconf.co.krgputechconf.comgputechconf.com.augputechconf.com.twgputechconf.eugputechconf.ingputechconf.jp nvidia.asia  nvidia.be  nvidia.ch nvidia.cn -cn nvidia.co.jp nvidia.co.kr -nvidia.com nvidia.com.au nvidia.com.br nvidia.com.tr nvidia.com.tw  nvidia.cz  nvidia.de  nvidia.dk  nvidia.es  nvidia.eu  nvidia.fi  nvidia.fr  nvidia.it  nvidia.lu  nvidia.nl  nvidia.no  nvidia.pl  nvidia.ro  nvidia.ru  nvidia.seshotwithgeforce.com -oracle -oracle.comoraclecloud.comoraclefoundation.org oracleimg.comateam-oracle.com -bronto.comcovid19-rx.org covid19rx.org sun.com  razer.comrazersupport.com razerzone.com razerzone.jprespawnbyrazer.com appcloud.comappexchange.com attic.iobeyondcore.combuddymedia.com chatter.comcloudcraze.com cotweet.com data.comdemandware.com desk.comdocumentforce.comdreamforce.com einstein.comexacttarget.com  force.com govforce.comgravitytank.com heywire.com krux.commarketingcloud.com metamind.io -pardot.com quotable.com radian6.com relateiq.comsalesforce.comsalesforce.orgsalesforceiq.comsalesforceliveagent.com salesforcemarketingcloud.com sequence.comsfdcstatic.com -sforce.com site.com -social.comsteelbrick.com twinprime.comweinvoiceit.com samsung -xn--cg4bkigalaxyappstore.comgalaxymobile.jpgame-platform.net knoxemm.com samsung.comsamsungapps.comsamsungcloud.com samsungdm.comsamsungeshop.com.cn -cnsamsunggalaxyfriends.comsamsungknox.comsamsungqbe.comsonyginzasonypark.jpsony-africa.com sony-asia.comsony-europe.comsony-hes.co.jpsony-latin.com sony-mea.comsony-olympus-medical.comsony-promotion.eusony-semicon.co.jp sony.at sony.ba sony.be sony.bg sony.ca sony.ch sony.cl -sony.co.cr -sony.co.id -sony.co.in -sony.co.jp -sony.co.kr -sony.co.nz -sony.co.th -sony.co.uk sony.com sony.com.ar sony.com.au sony.com.bo sony.com.br sony.com.co sony.com.do sony.com.ec sony.com.gt sony.com.hk sony.com.hn sony.com.mk sony.com.mx sony.com.my sony.com.ni sony.com.pa sony.com.pe sony.com.ph sony.com.sg sony.com.sv sony.com.tr sony.com.tw sony.com.vn sony.cz sony.de sony.dk sony.ee sony.es sony.eu sony.fi sony.fr sony.gr sony.hr sony.hu sony.ie sony.it sony.kz sony.lt sony.lu sony.lv sony.net sony.nl sony.no sony.pl sony.pt sony.ro sony.rs sony.ru sony.se sony.si sony.sk sony.ua sonybo.co.jp sonybsc.comsonybuilding.jpsonydesign.com sonydna.comsonyglobalsolutions.jp sonykigyo.jpsonylatvija.com -sonypcl.jpsonyprotechnosupport.co.jpsonystoragemedia.co.jp sonytc.co.jp  sovec.net -spacex.comboringcompany.com x.comads-twitter.com periscope.tv pscp.tvt.co tellapart.com tweetdeck.com  twimg.com twitpic.com twitter.com -twitter.jptwitterinc.comtwitteroauth.comtwitterstat.us twtrdns.net vine.couber-assets.com uber.com ubereats.com -oath.cloud oath.com ouroath.comstatic-verizon.com verizon.com verizon.netverizonbusinessfios.comverizonenterprise.comverizonfios.comverizonmedia.comverizonwireless.com vzw.com -xo.com mvk.com userapi.com  vk-cdn.me vk-portal.net vk.cc -vk.com  vk.design vk.link vk.me vkcache.com vkgo.app -vklive.appvkmessenger.comvkmessenger.app vkontakte.ruvkuseraudio.comvkuserlive.netvkuservideo.comvkuservideo.netg-technology.com -sandisk.ae -sandisk.it -sandisk.cn -cn sandisk.co.jp sandisk.co.kr sandisk.co.uk sandisk.comsandisk.com.ausandisk.com.brsandisk.com.trsandisk.com.tw -sandisk.de -sandisk.es -sandisk.fr -sandisk.hk -sandisk.id -sandisk.in -sandisk.nl -sandisk.sg -wd.com wdc.comwesterndigital.com -flickr -flickr.com -flickr.netstaticflickr.com ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -ads yahoo yahoo.ae yahoo.am yahoo.as yahoo.at yahoo.az yahoo.ba yahoo.be yahoo.bg yahoo.bi yahoo.bs yahoo.bt yahoo.by yahoo.ca  yahoo.cat yahoo.cd yahoo.cg yahoo.ch yahoo.cl yahoo.cmyahoo.cn -cn yahoo.co.ao yahoo.co.bw yahoo.co.ck yahoo.co.cr yahoo.co.id yahoo.co.il yahoo.co.in yahoo.co.jp yahoo.co.kr yahoo.co.mz yahoo.co.nz yahoo.co.th yahoo.co.tz yahoo.co.uk yahoo.co.uz yahoo.co.ve yahoo.co.vi yahoo.co.za  yahoo.com yahoo.com.af yahoo.com.ag yahoo.com.ai yahoo.com.ar yahoo.com.au yahoo.com.bd yahoo.com.bo yahoo.com.br yahoo.com.bz yahoo.com.cn -cn yahoo.com.co yahoo.com.do yahoo.com.ec yahoo.com.eg yahoo.com.es yahoo.com.fj yahoo.com.gi yahoo.com.gt yahoo.com.hk yahoo.com.kw yahoo.com.lb yahoo.com.ly yahoo.com.mt yahoo.com.mx yahoo.com.my yahoo.com.na yahoo.com.nf yahoo.com.om yahoo.com.pa yahoo.com.pe yahoo.com.ph yahoo.com.pk yahoo.com.pr yahoo.com.py yahoo.com.sa yahoo.com.sb yahoo.com.sg yahoo.com.sv yahoo.com.tj yahoo.com.tr yahoo.com.tw yahoo.com.ua yahoo.com.uy yahoo.com.vc yahoo.com.vn yahoo.cz yahoo.de yahoo.dj yahoo.dk yahoo.dm yahoo.ee yahoo.es yahoo.fi yahoo.fm yahoo.fr yahoo.ge yahoo.gg yahoo.gl yahoo.gm yahoo.gp yahoo.gr yahoo.gy yahoo.hk yahoo.hr yahoo.hu yahoo.ie yahoo.im yahoo.in yahoo.is yahoo.it yahoo.je yahoo.jo yahoo.la yahoo.lt yahoo.lu yahoo.lv yahoo.md yahoo.me yahoo.mk yahoo.mw yahoo.mx  yahoo.net yahoo.nl yahoo.no yahoo.nu yahoo.ph yahoo.pl yahoo.pn yahoo.ps yahoo.pt yahoo.ro yahoo.ru yahoo.rw yahoo.se yahoo.sg yahoo.sh yahoo.si yahoo.sk yahoo.sm yahoo.sn yahoo.so yahoo.sr yahoo.st yahoo.tg yahoo.tk yahoo.tl yahoo.tm yahoo.tn yahoo.vg yahoo.ws maktoob.com -myguide.hkyahoo-news.com.hk yahooapis.com yahoodns.netyahoofinance.comyahoohealth.comyahoomusic.comyahoosportsbook.com yho.com yimg.com  ymail.comxn--d1acpjx3f.xn--p1ai  yandex.az  yandex.by yandex.co.il -yandex.com yandex.com.am yandex.com.ge yandex.com.tr  yandex.ee  yandex.kg  yandex.kz  yandex.lt  yandex.lv  yandex.md -yandex.net  yandex.ru  yandex.st  yandex.sx  yandex.tj  yandex.tm  yandex.ua  yandex.uzyandexcloud.net yastatic.net -yandex 928.plus -zind.cloud aaex.uk amytele.com -ark.tobaicaonetwork.com bighead.groupblinkload.zone boslife.biz boslife.net  cloudn.me  conair.me cordcloud.org ctyun.online -cylink.procylink0122.icu dlercloud.com dlercloud.org dlercloud.me dleris.best duyaoss.com duyaossr.com hitun.ioxn--90wwvt03e.comjustmysocks.netjustmysocks1.netjustmysocks2.netjustmysocks4.net  maying.coxn--i2ru8q2qg.commocloudplus.com  mray.club muncloud.dog n3ro.fun  n3ro.live n3ro.net nexitally.comnexitallysafe.com nexitcore.com paofu.cloud paofuyun.me renzhe.cloud rixcloud.comshadowsocks.comshadowsocks.nlsquirrelvpn.comsquirrelgroup.net -ssplive.pwchina-internet-exchange.com cxkcloud.com ssrcloud.com ssrcloud.org -ssrpass.pwstc-server.com surflite.co surflite.net vilavpn.com vilavpn.xyz vilavpn1.xyz vilavpn2.xyz vilavpn3.xyz vilavpn4.xyz vilavpn5.xyz vilavpn6.xyz vilavpn7.xyz cursecdn.com -fandom.com  wikia.com  wikia.org dndbeyond.com futhead.com gamepedia.com muthead.comwikia.nocookie.netfandom.zendesk.com miraheze.org scp-wiki.net  scpdb.org scpwiki.comtranslatewiki.nettranslatewiki.org wdfiles.com wikidot.com wikihow.com mediawiki.org wikibooks.org wikidata.orgwikimediafoundation.org wikimedia.org wikinews.org wikipedia.org wikiquote.orgwikisource.orgwikiversity.orgwikivoyage.orgwiktionary.orgwmfusercontent.org -w.wiki toolforge.orgwikimediacloud.org wmcloud.org wmflabs.orgfamifun.com.tw family.co.jp family.com.twfamilymart.com.cn -cnfamilymart.com.myfamilymart.com.ph  famima.vn hkedcity.nethkreadingcity.nethuanghuagang.orgmcd  mcdonalds4008-517-517.cn -cnaboutmcdonalds.comhappymeal.co.nzhappymeal.com.au mcd.commcdelivery.co.idmcdelivery.co.krmcdelivery.com.aumcdelivery.com.mymcdelivery.com.sgmcdelivery.com.twmcdindonesia.com mcdonalds.camcdonalds.co.uk mcdonalds.commcdonalds.com.hk mcdonalds.hk mcdonalds.it mcdonalds.no mcdonalds.semcdonaldsarabia.commcdonaldsparties.co.nzmcdonaldsparties.com.au!xn--qhrx81fxh2a.xn--55qx5d.hk rarbg.me rarbg.to rarbg.israrbgaccess.org rarbggo.orgrarbgmirror.comrarbgmirror.orgrarbgproxy.org rarbgprx.orgrarbgunblock.comaerogard.com.auaerogard.com.cn -cnaerogardcn.com -airwick.at -airwick.be -airwick.ca -airwick.ch -airwick.cl airwick.co.in airwick.co.nz airwick.co.uk airwick.co.za airwick.comairwick.com.auairwick.com.brairwick.com.cn -cnairwick.com.mxairwick.com.tr -airwick.cz -airwick.de -airwick.dk -airwick.es -airwick.fi -airwick.fr -airwick.hr -airwick.hu -airwick.ie -airwick.it -airwick.jp airwick.net -airwick.nl -airwick.no -airwick.pl -airwick.pt -airwick.ro -airwick.ru -airwick.se -airwick.si -airwick.sk -airwick.tv -airwick.usairwickarabia.com vipoo.es  calgon.at  calgon.be  calgon.ch calgon.cn -cn calgon.co.uk -calgon.com calgon.com.tr  calgon.de  calgon.es  calgon.fr  calgon.ie  calgon.it  calgon.nl  calgon.pt  calgon.ru  calgon.tvcalgoncarbon-china.comcalgoncarbon.com calgonit.com clearasil.cn -cnclearasil.com.cn -cn clearasil.usdettol-prize.com  dettol.at  dettol.be  dettol.ch  dettol.cl dettol.co.id dettol.co.in dettol.co.ke dettol.co.nz dettol.co.uk -dettol.com dettol.com.au dettol.com.bd dettol.com.br dettol.com.cn -cn dettol.com.eg dettol.com.hk dettol.com.my dettol.com.ng dettol.com.sg  dettol.cz  dettol.fr  dettol.hu  dettol.ie -dettol.net  dettol.nl  dettol.pk  dettol.pl  dettol.pt  dettol.rudettolarabia.comdettolcleannaija.comdettolsitishield.co.indettolthailand.comespadoldettol.com.ardewitwithdurex.com dotherex.com durex-shop.chdurex-shopline.comdurex-slovenia.si durex.at durex.be durex.cl durex.co.id durex.co.il durex.co.nz durex.co.th durex.co.uk durex.co.za  durex.com durex.com.au durex.com.bd durex.com.cn -cn durex.com.co durex.com.hr durex.com.my durex.com.ng durex.com.pe durex.com.ph durex.com.pk durex.com.sg durex.com.tr durex.cz durex.de durex.dk durex.ee durex.es durex.fi durex.fr durex.hu durex.ie durex.it durex.jp durex.lv durex.mx durex.nl durex.no durex.pl durex.pt durex.ro durex.ru durex.se durex.us durexcam.comdurexcanada.comdurexchina.comdurexchina.com.cn -cndurexindia.comdurexloveclub.comdurexukraine.com durexusa.comlovesexdurex.cn -cnlovesexdurex.complaybydurex.com shopdurex.com trydurex.net trydurex.tvbebepremium3.com.bo -enfa.co.id enfa.com.vn enfaaplus.com enfababy.com enfabebe.comenfabebe.com.brenfabebe.com.coenfabebe.com.doenfabebe.com.ecenfabebe.com.mxenfabebe.com.peenfabebe.com.veenfabebe3.com.arenfagrow.co.inenfagrow.com.bnenfagrow.com.myenfagrow.com.phenfagrow.com.sg enfagrow4.comenfamama.com.arenfamama.com.ph -enfamil.ca enfamil.com -enfamil.es -enfamil.pl -enfamil.pt enfasmart.comenspireformula.comnutramigen.net nutramigen.pl$dishwashingexpert.com.cn -cn  finish.at  finish.bg finish.co.kr finish.co.nz finish.co.uk finish.co.za finish.com.cn -cn finish.com.hr finish.com.tr  finish.de  finish.es  finish.fr  finish.gr  finish.hu  finish.lv  finish.pl  finish.pt  finish.ro  finish.si  finish.skfinisharabia.comfinishbrasil.com.brfinishdishwashing.cafinishdishwashing.com finishinfo.be finishinfo.clfinishinfo.comfinishinfo.com.arfinishinfo.com.au finishinfo.cz finishinfo.fi finishinfo.it finishinfo.jp finishinfo.nl finishinfo.no finishinfo.ru finishinfo.sefinishkilpailu.fi finishwin.beyarininsuyu.com lizol.co.in lysol.ca lysol.cllysol.cn -cn lysol.co.cr  lysol.com lysol.com.cn -cn lysol.com.mx  lysol.netenfinitas.com.cn -cnmeadjohnson.commeadjohnson.com.cn -cnmeadjohnson.com.hkmeadjohnson.com.twmeadjohnson.net -mortein.cn -cn mortein.co.in mortein.co.nz mortein.co.za mortein.commortein.com.aumortein.com.brmortein.com.ngmortein.com.pkmorteincam.compifpafarabia.comkeepmovingwithmovefree.com move-free.net movefree.cn -cn movefrees.commovefreeoffers.commovefreerewards.comschiffvitamins.com -mucinex.ca -mucinex.cn mucinex.co.nz mucinex.commucinex.com.cn mucinex.net -mucinex.tvmucinexprofessional.com -nurofen.at -nurofen.bg -nurofen.cn -cn nurofen.co.il nurofen.co.nz nurofen.co.uk nurofen.co.za nurofen.comnurofen.com.aunurofen.com.cn -cnnurofen.com.sg -nurofen.cz -nurofen.de -nurofen.es -nurofen.fr -nurofen.hr -nurofen.hu -nurofen.ie -nurofen.it nurofen.net -nurofen.no -nurofen.pl -nurofen.pt -nurofen.ro -nurofen.sk -nurofen.runurofengel.com51nurofensk-prod-env.eu-west-1.elasticbeanstalk.com -cepacol.ca cepacol.com dobendan.degraneodin.com.mx -repsils.no strepsils.at strepsils.ch strepsils.cn -cnstrepsils.co.krstrepsils.co.nzstrepsils.co.zastrepsils.com.arstrepsils.com.brstrepsils.com.cn -cnstrepsils.com.costrepsils.com.austrepsils.com.hkstrepsils.com.phstrepsils.com.tw strepsils.es strepsils.fi strepsils.fr strepsils.hr strepsils.hu strepsils.ie strepsils.net strepsils.pl strepsils.pt strepsils.ro strepsils.ru strepsils.si strepsils.usstrepsilsarabia.como2action.co.krpowerofresolve.capowerofresolve.comspraynwash.com  vanish.at  vanish.be  vanish.ch  vanish.cl vanish.cn -cn vanish.co.id vanish.co.il vanish.co.in vanish.co.nz vanish.co.uk vanish.co.za vanish.com.ar vanish.com.br vanish.com.co vanish.com.hr vanish.com.mx vanish.com.my vanish.com.pe vanish.com.sg vanish.com.tr  vanish.de  vanish.dk  vanish.es  vanish.fi  vanish.fr  vanish.hu  vanish.ie  vanish.it  vanish.nl  vanish.no  vanish.pl  vanish.pt  vanish.ro  vanish.ru  vanish.se  vanish.si  vanish.skvanisharabia.comvanishbancaseulook.com.brvanishcentroamerica.com vanishinfo.czvanishstains.comvanishstains.com.au veet.at veet.be veet.ca veet.ch veet.clveet.cn -cn -veet.co.id -veet.co.in -veet.co.kr -veet.co.nz -veet.co.uk -veet.co.za veet.com veet.com.ar veet.com.au veet.com.bd veet.com.br veet.com.cn -cn veet.com.co veet.com.hk veet.com.mx veet.com.my veet.com.ph veet.com.pk veet.com.sg veet.com.tr veet.de veet.dk veet.es veet.fi veet.fr veet.hu veet.jp veet.nl veet.no veet.pt veet.ro veet.ru veet.se veet.tv veet.usveetarabia.comveetbangladesh.comveetcentroamerica.com veetclub.it -woolite.ca -woolite.cn -cn woolite.comwoolite.com.cn -cn -woolite.pl -woolite.uswoolitecarpet.comdigital-rb.com rb-crisis.com -rb.com -rb.netrbbusinessshop.comrbdigitalab.comrbdigitallab.comrbeuroinfo.com rbgrads.comrbgraduates.comrbmavericks.comrbnainternational.com  rbplc.comrbrandlibrary.com rbspeakup.com reckitt.netreckittbenckiser.cn -cnreckittbenckiser.com#reckittbenckiser.com.cn -cnreckittbenckiser.netreckittbenckiser.tvreckittprofessional.com starbucks.castarbucks.co.uk starbucks.comstarbucks.com.cn -cn starbucks.de starbucks.fr starbucks.ie -tsquare.tv  windy.com xdty.org  xingrz.me -+ -GUARDIANPROJECTguardianproject.info - - HASHICORP  consul.io hashicorp.comnomadproject.io  packer.io terraform.io vagrantup.comvaultproject.io -- - PUBMATIC-ADSads.pubmatic.com -ads -' - WTEAM-ADS ads.wteam.xyz -ads - -ZERONET -zeronet.io - -ELIXIRelixir-lang.org -: -ESRI -arcgis.comarcgisonline.com esri.com -+ -MONGODB mongodb.com mongodb.org -T - DLERCLOUD dlercloud.com dlercloud.org dlercloud.me dleris.best - -HQPORNER hqporner.com -T - -PROTONMAIL protonmail.chprotonmail.comprotonstatus.com pm.me -) - REBRANDLY rb.gy rebrandly.com - -VIMEO -vhx.tv -vhxqa1.com -vhxqa2.com -vhxqa3.com -vhxqa4.com -vhxqa6.comvimeo-staging.comvimeo-staging2.com  vimeo.com vimeo.frvimeobusiness.com vimeocdn.comvimeogoods.comvimeoondemand.comvimeostatus.comlivestream.com - -3DM 3dmgame.com - -CASTRO  castro.fm - - -ITUNES!12diasdepresentesdeitunes.com12diasderegalosdeitunes.cl12diasderegalosdeitunes.co!12diasderegalosdeitunes.co.cr!12diasderegalosdeitunes.co.ni!12diasderegalosdeitunes.co.ve12diasderegalosdeitunes.com"12diasderegalosdeitunes.com.co"12diasderegalosdeitunes.com.hn"12diasderegalosdeitunes.com.ni"12diasderegalosdeitunes.com.ve12diasderegalosdeitunes.cr12diasderegalosdeitunes.gt12diasderegalosdeitunes.hn12diasderegalosdeitunes.pe 12joursdecadeauxdeitunes.comapple-itunes.cn -cn itun.es itunbes.com ituneas.comitunes-apple.cn -cnitunes-radio.net  itunes.ca  itunes.co itunes.co.th -itunes.com  itunes.hk  itunes.mx -itunes.org -itunes.rio  itunes.usitunes12days.comitunesapple.cn -cnitunesessentials.comitunesfestivals.comitunesiradio.comituneslatino.comituneslogin.netitunesmatch.comitunesparty.comitunesradio.cn -cnitunesradio.comitunesradio.com.cn -cnitunesradio.rioitunesradio.tvitunesradio.twitunesstore.co itunesu.com itunesu.net iutunes.com wwwitunes.comitunes-apple.com.akadns.net -+ -FORBES -forbes.com forbesimg.com -3 - SEGMENTFAULTsegmentfault.com  sifou.com -f - TAYLORFRANCIS tandf.co.uktandfonline.comtaylorandfrancis.comtaylorfrancis.com - -ANTUTU -antutu.com -+ -LINKEDIN linkedin.com  licdn.com -7 -AMD amd.comamdfanstore.com -radeon.com - -SFACG  sfacg.com -\ -TAIKANG taikang.comtaikanglife.comtaikangzhijia.comtkhealthcare.com - -TENCENT dns.pub  dnsapi.cn  dnspod.cn -dnspod.com dnspod.com.cn -dnspod.net -dnspod.org  dnsv1.com  dnsv2.com  dnsv3.com  dnsv4.com  dnsv5.com doh.pub ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads -yuewen.com hongxiu.com lrts.me qdmm.com -qidian.com readnovel.com tingbook.com webnovel.com -xs8.cn xxsy.net -apcdns.net  cdn-go.cn cdntips.com foxmail.comfoxmail.com.cn gtimg.cn  gtimg.com gtimg.com.cn idqqimg.com imqq.com  myapp.com myqcloud.com -qcloud.com qcloudimg.com qlogo.cn qpic.cn -qq.com -qqmail.com  qzone.com -rtx.com.cnservicewechat.com smtcdns.com smtcdns.net -tencent.cn tencent.comtencent.com.cntencent.com.hktencent.net.cntencent-cloud.comtencentmusic.comtencentyun.com -tenpay.com -wechat.com -wegame.com wegame.com.cn -weiyun.com alloyteam.com imweb.io ivweb.io - -EA dawngate.com eastore.comlordofultima.com -origin.com  origin.tv#cloudsync-prod.s3.amazonaws.comorigin-a.akamaihd.netrtm.tnt-ea.comssl-lvlt.cdn.ea.comawayoutgame.combattlefieldbadcompany2.combattlefront2.combattlefrontii.combejewled-stars.combejeweledstars.comcapitalgames.com chillingo.comcommandandconquer.comconquerwithcharacter.com  crysis.jp dawngate.comdawngatechronicles.com -dicela.com -dicela.netea-anz-press.com -ea.com eaaccess.comeacashcard.com eacodigos.com eakorea.co.kr eamobile.com eanordic.com -eaplay.com easports.com easports.jpeasportsactive.comeasportsactiveonline.comeasportsfootball.comeasportsfootballclub.comeasportsmma.comeasportsworld.comelectronicarts.comelectronicarts.fr futpromos.com futunited.comghostgames.comheroesofdragonage.com hutpromos.commaddenchampionship.commaddenrewards.commaddenseason.infomaddenseason.netmaddenseason.org  maxis.commysimsracing.com play4free.com playapex.com pogo.com pogobeta.com -popcap.comprojectapex.com pvzheroes.com spearhead.krspeedhunters.com  spore.com ssx3.comstarwarsbattlefront.comstarwarsbattlefront2.comtiberiumalliances.com tiburon.com -tnt-ea.comultimaforever.com unravel2.comunraveltwo.comvisceralgames.com -yogify.comanthemgame.comanthemthegame.comapexlegends.comprojectapex.combattlefield.combattlefield1943.combattlefield3.combattlefield4.combattlefield5.combattlefieldheroes.combattlefieldv.com battlelog.com bioware.combiowarestore.com eamythic.com eamythic.net cncrivals.comcommandandconquer.comtiberiumalliances.comcamelot-europe.comcamelotherald.comcamelotherald.net daoc.netdarkageofcamelot.comdarkness-risen.comdeadspacegame.com dice.se dragonage.comdragonagekeep.comdragonagemovie.comthedreadwolfrises.comdungeonkeeper.cndungeonkeeper.comdungeonkeeper.com.cneafootballworld.comfifastreet.comfifastreet3.com frostbite.comindustrialtoys.commasseffect.commasseffectarchives.comeamirrorsedge.commirrorsedge.commirrorsedge.jpmirrorsedge.netmirrorsedge2.commirrorsedge2d.comblackboxgames.comeablackbox.comneedforspeed.comneedforspeedboost.comneedforspeeddriftkings.comneedforspeedeliminator.comneedforspeedlightning.comneedforspeedoverdrive.comneedforspeedproven.comneedforspeedredline.comneedforspeedshowdown.comneedforspeedstreetkings.comneedforspeedtakedown.comneedforspeedtherun.comneedforspeedtimeattack.com#needforspeedundergroundeast.com nfsworld.comteamneedforspeed.complantsvszombies2.com -pvzgw2.com pvzheroes.comseaofsolitude.comsimcity-buildit.com simcity.com -skate2.com spearhead.krstarwarsfallenorder.comstarwarsjedifallenorder.comstarwarstheoldrepublic.comswjedifallenorder.com  swjfo.com  swtor.com  swtor.net -mysims.com thesims.com thesims3.com thesims4.comthesimssocial.com titanfall.commythicentertainment.commythicentertainment.netmythicgames.comultimaonline.com -uo.com uoherald.com xdsummit.comea.tt.omtrdc.neteaassets-a.akamaihd.net -= - JAVFINDER javfinder.sh findercdn.me o0-2.com - -PORNHD -pornhd.com - -YANDEXxn--d1acpjx3f.xn--p1ai  yandex.az  yandex.by yandex.co.il -yandex.com yandex.com.am yandex.com.ge yandex.com.tr  yandex.ee  yandex.kg  yandex.kz  yandex.lt  yandex.lv  yandex.md -yandex.net  yandex.ru  yandex.st  yandex.sx  yandex.tj  yandex.tm  yandex.ua  yandex.uzyandexcloud.net yastatic.net -yandex - -CENTOS -centos.org - -GOOGLE-SCHOLARscholar.google.aescholar.google.atscholar.google.bescholar.google.bgscholar.google.cascholar.google.catscholar.google.chscholar.google.clscholar.google.cnscholar.google.co.crscholar.google.co.idscholar.google.co.ilscholar.google.co.inscholar.google.co.jpscholar.google.co.krscholar.google.co.nzscholar.google.co.thscholar.google.co.ukscholar.google.co.vescholar.google.co.zascholar.google.comscholar.google.com.arscholar.google.com.auscholar.google.com.boscholar.google.com.brscholar.google.com.coscholar.google.com.cuscholar.google.com.doscholar.google.com.ecscholar.google.com.egscholar.google.com.gtscholar.google.com.hkscholar.google.com.lyscholar.google.com.mxscholar.google.com.myscholar.google.com.nischolar.google.com.pascholar.google.com.pescholar.google.com.phscholar.google.com.pkscholar.google.com.prscholar.google.com.pyscholar.google.com.sgscholar.google.com.svscholar.google.com.trscholar.google.com.twscholar.google.com.uascholar.google.com.uyscholar.google.com.vnscholar.google.czscholar.google.descholar.google.dkscholar.google.esscholar.google.fischolar.google.frscholar.google.grscholar.google.hnscholar.google.hrscholar.google.huscholar.google.isscholar.google.itscholar.google.lischolar.google.ltscholar.google.luscholar.google.lvscholar.google.nlscholar.google.noscholar.google.plscholar.google.ptscholar.google.roscholar.google.ruscholar.google.sescholar.google.sischolar.google.sk - -SLASHDOT slashdot.org - -SHORTURL shorturl.at -0 -SOHU sohu.com -sohucs.com -itc.cn -4 -THEBORINGCOMPANYboringcompany.com x.com - -GOOGLE android.comandroidify.com blogspot.ae blogspot.al blogspot.am blogspot.ba blogspot.be blogspot.bg blogspot.bj blogspot.ca blogspot.cf blogspot.ch blogspot.clblogspot.co.atblogspot.co.idblogspot.co.ilblogspot.co.keblogspot.co.nzblogspot.co.ukblogspot.co.za blogspot.comblogspot.com.arblogspot.com.aublogspot.com.brblogspot.com.byblogspot.com.coblogspot.com.cyblogspot.com.eeblogspot.com.egblogspot.com.esblogspot.com.mtblogspot.com.ngblogspot.com.trblogspot.com.uy blogspot.cv blogspot.cz blogspot.de blogspot.dk blogspot.fi blogspot.fr blogspot.gr blogspot.hk blogspot.hr blogspot.hu blogspot.ie blogspot.in blogspot.is blogspot.it blogspot.jp blogspot.kr blogspot.li blogspot.lt blogspot.lu blogspot.md blogspot.mk blogspot.mx blogspot.my blogspot.nl blogspot.no blogspot.pe blogspot.pt blogspot.qa blogspot.re blogspot.ro blogspot.rs blogspot.ru blogspot.se blogspot.sg blogspot.si blogspot.sk blogspot.sn blogspot.td blogspot.tw blogspot.ug blogspot.vn dart.dev dartlang.org dartpad.dev fastlane.cifastlane.tools flutter.devflutterapp.com go-lang.com go-lang.net go-lang.org -go.dev  godoc.org -golang.com -golang.net -golang.org2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -adsscholar.google.aescholar.google.atscholar.google.bescholar.google.bgscholar.google.cascholar.google.catscholar.google.chscholar.google.clscholar.google.cnscholar.google.co.crscholar.google.co.idscholar.google.co.ilscholar.google.co.inscholar.google.co.jpscholar.google.co.krscholar.google.co.nzscholar.google.co.thscholar.google.co.ukscholar.google.co.vescholar.google.co.zascholar.google.comscholar.google.com.arscholar.google.com.auscholar.google.com.boscholar.google.com.brscholar.google.com.coscholar.google.com.cuscholar.google.com.doscholar.google.com.ecscholar.google.com.egscholar.google.com.gtscholar.google.com.hkscholar.google.com.lyscholar.google.com.mxscholar.google.com.myscholar.google.com.nischolar.google.com.pascholar.google.com.pescholar.google.com.phscholar.google.com.pkscholar.google.com.prscholar.google.com.pyscholar.google.com.sgscholar.google.com.svscholar.google.com.trscholar.google.com.twscholar.google.com.uascholar.google.com.uyscholar.google.com.vnscholar.google.czscholar.google.descholar.google.dkscholar.google.esscholar.google.fischolar.google.frscholar.google.grscholar.google.hnscholar.google.hrscholar.google.huscholar.google.isscholar.google.itscholar.google.lischolar.google.ltscholar.google.luscholar.google.lvscholar.google.nlscholar.google.noscholar.google.plscholar.google.ptscholar.google.roscholar.google.ruscholar.google.sescholar.google.sischolar.google.skpolymer-project.orgpolymerproject.org -v8.dev v8project.org youtubeggpht.cn -cn  ggpht.comgooglevideo.comwithyoutube.com youtu.beyoutube-nocookie.com -youtube.ae -youtube.al -youtube.am -youtube.at -youtube.az -youtube.ba -youtube.be -youtube.bg -youtube.bh -youtube.bo -youtube.by -youtube.ca youtube.cat -youtube.ch -youtube.cl -youtube.co youtube.co.ae youtube.co.at youtube.co.cr youtube.co.hu youtube.co.id youtube.co.il youtube.co.in youtube.co.jp youtube.co.ke youtube.co.kr youtube.co.ma youtube.co.nz youtube.co.th youtube.co.tz youtube.co.ug youtube.co.uk youtube.co.ve youtube.co.za youtube.co.zw youtube.comyoutube.com.aryoutube.com.auyoutube.com.azyoutube.com.bdyoutube.com.bhyoutube.com.boyoutube.com.bryoutube.com.byyoutube.com.coyoutube.com.doyoutube.com.ecyoutube.com.eeyoutube.com.egyoutube.com.esyoutube.com.ghyoutube.com.gryoutube.com.gtyoutube.com.hkyoutube.com.hnyoutube.com.hryoutube.com.jmyoutube.com.joyoutube.com.kwyoutube.com.lbyoutube.com.lvyoutube.com.lyyoutube.com.mkyoutube.com.mtyoutube.com.mxyoutube.com.myyoutube.com.ngyoutube.com.niyoutube.com.omyoutube.com.payoutube.com.peyoutube.com.phyoutube.com.pkyoutube.com.ptyoutube.com.pyyoutube.com.qayoutube.com.royoutube.com.sayoutube.com.sgyoutube.com.svyoutube.com.tnyoutube.com.tryoutube.com.twyoutube.com.uayoutube.com.uyyoutube.com.ve -youtube.cr -youtube.cz -youtube.de -youtube.dk -youtube.ee -youtube.es -youtube.fi -youtube.fr -youtube.ge -youtube.gr -youtube.gt -youtube.hk -youtube.hr -youtube.hu -youtube.ie -youtube.in -youtube.iq -youtube.is -youtube.it -youtube.jo -youtube.jp -youtube.kr -youtube.kz -youtube.la -youtube.lk -youtube.lt -youtube.lu -youtube.lv -youtube.ly -youtube.ma -youtube.md -youtube.me -youtube.mk -youtube.mn -youtube.mx -youtube.my -youtube.ng -youtube.ni -youtube.nl -youtube.no -youtube.pa -youtube.pe -youtube.ph -youtube.pk -youtube.pl -youtube.pr -youtube.pt -youtube.qa -youtube.ro -youtube.rs -youtube.ru -youtube.sa -youtube.se -youtube.sg -youtube.si -youtube.sk -youtube.sn youtube.soy -youtube.sv -youtube.tn -youtube.tv -youtube.ua -youtube.ug -youtube.uy -youtube.vnyoutubeeducation.comyoutubegaming.comyoutubego.co.idyoutubego.co.in youtubego.comyoutubego.com.br youtubego.id youtubego.inyoutubei.googleapis.comyoutubekids.comyoutubemobilesupport.com yt.be  ytimg.comand -chromedclkgbizgle gmailgoogoog -googleguge hangout nexus xn--qcka1pmc  google.ad  google.ae  google.al  google.am  google.as  google.at  google.az  google.ba  google.be  google.bf  google.bg  google.bi  google.bj  google.bs  google.bt  google.by  google.ca -google.cat  google.cd  google.cf  google.cg  google.ch  google.ci  google.cl  google.cm google.cn -cn google.co.ao google.co.bw google.co.ck google.co.cr google.co.id google.co.il google.co.in google.co.jp google.co.ke google.co.kr google.co.ls google.co.ma google.co.mz google.co.nz google.co.th google.co.tz google.co.ug google.co.uk google.co.uz google.co.ve google.co.vi google.co.za google.co.zm google.co.zw -google.com google.com.af google.com.ag google.com.ai google.com.ar google.com.au google.com.bd google.com.bh google.com.bn google.com.bo google.com.br google.com.bz google.com.co google.com.cu google.com.cy google.com.do google.com.ec google.com.eg google.com.et google.com.fj google.com.gh google.com.gi google.com.gt google.com.hk google.com.jm google.com.kh google.com.kw google.com.lb google.com.ly google.com.mm google.com.mt google.com.mx google.com.my google.com.na google.com.ng google.com.ni google.com.np google.com.om google.com.pa google.com.pe google.com.pg google.com.ph google.com.pk google.com.pr google.com.py google.com.qa google.com.sa google.com.sb google.com.sg google.com.sl google.com.sv google.com.tj google.com.tr google.com.tw google.com.ua google.com.uy google.com.vc google.com.vn  google.cv  google.cz  google.de  google.dj  google.dk  google.dm  google.dz  google.ee  google.es  google.fi  google.fm  google.fr  google.ga  google.ge  google.gg  google.gl  google.gm  google.gr  google.gy  google.hn  google.hr  google.ht  google.hu  google.ie  google.im  google.iq  google.is  google.it  google.je  google.jo  google.kg  google.ki  google.kz  google.la  google.li  google.lk  google.lt  google.lu  google.lv  google.md  google.me  google.mg  google.mk  google.ml  google.mn  google.ms  google.mu  google.mv  google.mw  google.ne  google.nl  google.no  google.nr  google.nu  google.pl  google.pn  google.ps  google.pt  google.ro  google.rs  google.ru  google.rw  google.sc  google.se  google.sh  google.si  google.sk  google.sm  google.sn  google.so  google.sr  google.st  google.td  google.tg  google.tl  google.tm  google.tn  google.to  google.tt  google.vg  google.vu  google.ws 0emm.com  1e100.net  1ucrs.com -466453.com abc.xyz adgoogle.net -admeld.comangulardart.org -api.ai -apigee.com appbridge.ca appbridge.io appbridge.it appspot.com -apture.com area120.com -asp-cc.com autodraw.com bandpage.combaselinestudy.combaselinestudy.org bazel.build bdn.devbeatthatquote.com  blink.org blogblog.com blogger.combrocaproject.com -brotli.org bumpshare.com -bumptop.ca bumptop.com bumptop.net bumptop.org bumptunes.comcampuslondon.com certificate-transparency.org chat.gle -chrome.comchromebook.comchromecast.com chromeos.dev chromium.orgcloudburstresearch.comcloudfunctions.netcloudrobotics.comcobrasearch.com codespot.com conscrypt.com conscrypt.orgcookiechoices.org  coova.com  coova.net  coova.orgcrossmediapanel.com crr.com  cs4hs.comdartsearch.netdataliberation.org  debug.comdebugproject.com deepmind.comdevsitetest.howdialogflow.comdigitalassetlinks.org episodic.comfeedburner.com -fflick.comfinanceleadsonline.comfirebaseapp.comfirebaseio.com -foofle.com froogle.com fuchsia.dev  g-tun.comg.cn -cng.co g.dev -g.pagegerritcodereview.comgetbumptop.com ggoogle.com gipscorp.com gkecnapps.cn -cn globaledu.org  gmail.com gmodules.com  gogle.com -gogole.comgonglchuangl.net -goo.gl -googel.com -googil.com  googl.comgoogle-syndication.com google.berlin -google.dev -google.net -google.orggoogle.ventures"googleacquisitionmigration.com googleapis.cn -cngoogleapis.comgoogleapps.comgooglearth.comgoogleblog.com googlebot.comgooglecapital.comgooglecert.netgooglecnapps.cn -cngooglecode.comgooglecommerce.comgooglecompare.co.ukgoogledanmark.comgoogledomains.comgoogledrive.com googlee.comgoogleearth.comgooglefiber.netgooglefinland.comgooglemail.comgooglemaps.comgooglepagecreator.comgooglephotos.comgoogleplay.comgoogleplus.comgooglesource.comgooglestore.comgooglesverige.comgoogleusercontent.comgoogleventures.com -googlr.com -goolge.com gooogle.com gridaware.app gsrc.io -gstatic.cn -cn gstatic.comgstaticcnapps.cn -cn -gsuite.com -gv.com gvt0.com gvt1.com gvt2.com gvt3.com gvt5.com gvt6.com gvt7.com gvt9.comhdrplusdata.org hey.gle hindiweb.comhowtogetmo.co.ukhtml5rocks.com hwgo.comiamremarkable.org igoogle.com impermium.com -j2objc.orgjibemobile.com keyhole.comkeytransparency.comkeytransparency.fookeytransparency.org lanternal.com like.commadewithcode.com material.io mdialog.com meet.newmfg-inspector.commobileview.pagemoodstocks.com near.by nest.com nomulus.foo -oauthz.com -ok.gle on.here on2.comonefifteen.netonefifteen.orgoneworldmanystories.com openthread.io openweave.io orbitera.com  page.linkpagespeedmobilizer.com pageview.mobi panoramio.compartylikeits1986.orgpaxlicense.org -picasa.com picasaweb.com picasaweb.net picasaweb.org -picnik.com pittpatt.com postini.comprojectara.comprojectbaseline.compublishproxy.comquestvisual.comquickoffice.com quiksee.com -revolv.comridepenguin.com run.appsavethedate.foo -saynow.com schemer.comscreenwisetrends.comscreenwisetrendspanel.com snapseed.com solveforx.com -stadia.devstcroixmosquito.comstcroixmosquitoproject.comstudywatchbyverily.comstudywatchbyverily.orgstxmosquito.comstxmosquitoproject.comstxmosquitoproject.netstxmosquitoproject.org synergyse.comtensorflow.org  tfhub.devthecleversense.comthegooglestore.comthinkquarterly.co.ukthinkquarterly.comthinkwithgoogle.com tiltbrush.com txcloud.net  txvia.comunfiltered.news useplannr.comusvimosquito.comusvimosquitoproject.comvelostrata.com -verily.comverilylifesciences.comverilystudyhub.comverilystudywatch.comverilystudywatch.org -wallet.com  waymo.com waze.com web.app web.devwebappfieldguide.comwebmproject.orgwebpkgcache.com -webrtc.orgweltweitwachsen.dewhatbrowser.org widevine.comwithgoogle.com womenwill.comwomenwill.com.br womenwill.id womenwill.in womenwill.mx  x.company -x.team xn--9kr7l.comxn--9trs65b.comxn--flw351e.com xplr.cozukunftswerkstatt.de265.com -cn2mdn.net -cn adservice.google.com -cnapp-measurement.com -cn beacons.gcp.gvt2.com -cnbeacons.gvt2.com -cnbeacons3.gvt2.com -cn c.admob.com -cn(c.android.clients.google.com -cn!cache.pack.google.com -cncheckin.gstatic.com -cn%clickserve.dartsearch.net -cn)clientservices.googleapis.com -cn)connectivitycheck.gstatic.com -cncorp.google.com -cn crl.pki.goog -cn"csi-china.l.google.com -cncsi.gstatic.com -cn dl.google.com -cndl.l.google.com -cndoubleclick.net -cn-!firebase-settings.crashlytics.com -cn fonts.googleapis.com -cnfonts.gstatic.com -cn google-analytics.com -cn googleadservices.com -cngoogleanalytics.com -cn!googlesyndication.com -cn googletagmanager.com -cn!googletagservices.com -cngtm.oasisfeng.com -cn!imasdk.googleapis.com -cn kh.google.com -cn ocsp.pki.goog -cn, pagead-googlehosted.l.google.com -cn!pki-goog.l.google.com -cn recaptcha.net -cnredirector.gvt1.com -cn)safebrowsing-cache.google.com -cn'safebrowsing.googleapis.com -cn$settings.crashlytics.com -cn-!ssl-google-analytics.l.google.com -cnssl.gstatic.com -cn%toolbarqueries.google.com -cntools.google.com -cntools.l.google.com -cn$translate.googleapis.com -cn!update.googleapis.com -cn-!www-googletagmanager.l.google.com -cnwww.gstatic.com -cnwww.recaptcha.net -cn0$^khm([0-3]|db)?\.google(apis)?\.com$ -cn -1 -N3RO n3ro.fun  n3ro.live n3ro.net - -SAMSUNG samsung -xn--cg4bkigalaxyappstore.comgalaxymobile.jpgame-platform.net knoxemm.com samsung.comsamsungapps.comsamsungcloud.com samsungdm.comsamsungeshop.com.cn -cnsamsunggalaxyfriends.comsamsungknox.comsamsungqbe.com -, -RENREN -renren.comrenren-inc.com - - SONYPICTURESimageworks.commotionpictureser.com playshow.ioplayshowtv.comsonyclassics.comsonypictures.comsonypicturesanimation.comsonypicturesmuseum.comsonypicturesstudios.comsonypicturestelevision.com#sonypicturestelevisiongames.com wheelpop.com -wwtbam.comyourfantasybeginsnow.com -q -CATEGORY-HOSPITAL-CN  fuwai.com pkuh6.cn puh3.net.cnhospital.pku.edu.cnss.bjmu.edu.cn -@ - DIGITALOCEANdigitalocean.comdigitaloceanspaces.com -a -DYNA dynacw.co.jp -dynacw.com dynacw.com.cn dynacw.com.hk dynacw.com.tw - -OPENJSFOUNDATION amp.devampproject.comampproject.netampproject.orgcdn.ampproject.org -cnamp.akamaized.netelectronjs.org -jquery.comjquerymobile.com jqueryui.com mochajs.org -nodejs.org openjsf.org - -PASTEBIN pastebin.com - - -KINDLE4RSSkindle4rss.com - - MEGAPHONE megaphone.fm - - NEWYORKER newyorker.com -0 - -SOUNDCLOUDsoundcloud.com -sndcdn.com - - TONGCHENG 40017.cn -17u.cn 17u.net 17usoft.com 17usoft.net  elong.comelongstatic.com -ly.com - - BREITBART breitbart.com -3 -HKEDCITY hkedcity.nethkreadingcity.net - -PATREON patreon.com - -GITEE  gitee.com -$ - HUANGHUAGANGhuanghuagang.org -B -MITmit mit.edu mit.netmitpressjournals.org -$ -NOTEPADnotepad-plus-plus.org - - PHANTOMJS phantomjs.org - -ADBLOCKgetadblock.com - -CHANGE -change.org - - EPICGAMESbattlebreakers.com epicgames.com fortnite.com paragon.complayparagon.comroborecall.comshadowcomplex.com spyjinx.comunrealengine.comunrealtournament.com6*^cdn\d-epicgames-\d+\.file\.myqcloud\.com$ -cn*&^epicgames-download\d\.akamaized\.net$;/^epicgames-download\d-\d+\.file\.myqcloud\.com$ -cn - -SSPAI  sspai.com -8 - -CYBERTRUSTcybertrust.co.jpcybertrust.ne.jp - -IEEE ieee.org ieee.tvtryengineering.org ieee-ccnc.org ieee-ecce.org ieeemce.orgstandardsuniversity.orgieee-region6.org ieee.ca ieeer10.org -ieeer5.org -ieeer8.org ieeeusa.orgieeefoundation.orgieee-into-focus.org ieee-vics.org ieeeday.orgdatasheets360.comglobalspec.com ieee-isto.orgtechstreet.com computer.org -comsoc.org embs.org emcs.org grss-ieee.org ieee-aess.org ieee-cas.org ieee-ceda.orgieee-edusociety.org ieee-ies.org ieee-ims.org ieee-npss.org ieee-pels.org ieee-pes.org ieee-ras.orgieee-sensors.org ieee-tems.org ieee-uffc.org ieeeaps.org ieeecsc.org ieeecss.org ieeedeis.orgieeemagnetics.org ieeenano.org ieeeoes.org ieeesmc.orgieeesystemscouncil.org  itsoc.org mtt.orgphotonicssociety.orgsignalprocessingsociety.orgtechnologyandsociety.org vtsociety.org -% -NGINX  nginx.com  nginx.org -+ -TRELLO -trello.com trellocdn.com - -ALI213 -ali213.net - -BILIPLUS biliplus.com - - STCSERVERstc-server.com -: -EBAY21centuryaccess.com4ebaytraders.com adcommerce.cn -cn adcommerce.tv appforebay.cn -cnappsonebay.net -asebay.com -baazee.com -bidbay.combidorbuyindia.com billpoint.combillpoint.info billpoint.tv billpoint.usbillpointnewzealand.com blogebay.combookclubcorner.combuiltfromebay.com buyitnow.com buyitnow.net buyitnow.org buyitnow.tvbuyitnowshop.net cafr.ca carebay.comcargigileads.com  cebay.comcollective99.comcommerceos.comconnectcommerce.cn -cn"connectcommerce.com.cn -cnconnectcommerce.hkconnectcommerce.infoconnectcommerce.tv connectedcommerce.cn -cnconnectedcommerce.comconnectedcommerce.tvcrececonebay.comcreditcardsbay.com cyber-bay.cn -cncyber-bay.com.cn -cncyber-bay.info cyber-bay.org -dba.dk dealbay.com dealtime.com  didce.com  douya.orgdreamtoplay.com  e-bay.com e-bay.it  e-bay.net eachpay.com eachpay.net  ebahy.comebay-authenticate.netebay-confirm.comebay-course.com ebay-cz.comebay-delivery.comebay-discoveries.comebay-fashion.com ebay-inc.com ebay-inc.net ebay-inc.orgebay-online.comebay-sales.comebay-stories.com ebay-us.comebay-vacation.com ebay.at ebay.be ebay.ca ebay.chebay.cn -cn -ebay.co.nz -ebay.co.uk -ebay.co.ve -ebay.co.za ebay.com ebay.com.ar ebay.com.au ebay.com.cn -cn ebay.com.ec ebay.com.hk ebay.com.mt ebay.com.my ebay.com.ph ebay.com.sg ebay.de ebay.es ebay.fr ebay.ie ebay.in ebay.it ebay.jp ebay.lt ebay.mn ebay.net.cn -cn ebay.nl ebay.org ebay.org.cn -cn ebay.ph ebay.pk ebay.pl ebay.sg ebay.us ebay.vn -ebay.yn.cn -cn -ebay.zj.cn -cn -ebay25.com -ebay68.com  ebaya.com ebayads.com ebayads.netebayadvertising.cn -cnebayadvertising.comebayanunsios.netebayauction.comebayaustralia.comebayauthenticate.com.cn ebaybags.com ebaybank.comebaybenefits.comebayboutique.com -ebayca.com -ebayca.org ebaycafe.com ebaycar.comebaycareers.com ebaycbt.co.kr ebaycdn.net ebaychina.netebayclassifiedsgroup.comebayclassifieds.cn -cnebayclassifieds.com"ebayclassifieds.com.cn -cnebayclassifieds.infoebayclassifieds.orgebayclassifieds.tvebayclassifiedsgroup.comebayclassifiedsgroup.infoebayclassifiedsgroup.orgebayclassifies.com ebayclub.com ebaycoins.com ebaycom.comebaycommercenetwork.comebaycourse.com  ebayd.com -ebayde.com ebaydesc.cn -cnebaydesc.com.cn -cnebaydlassifieds.com -ebaydns.cn -cn ebaydts.com ebayedu.comebayeletro.comebayenterprise.cn -cnebayenterprise.com!ebayenterprise.com.cn -cnebayenterprise.infoebayenterprise.netebayenterprise.tv ebayetc.comebayexpress.sgebayfashion.comebayfashion.netebayforcharity.orgebayforeclosure.orgebayfrance.comebayglobalshipping.com ebaygroup.com ebayhabit.com ebayheels.com ebayhots.com ebayimg.com ebayinc.com ebayinc.net ebayinc.org ebayincconnectedcommerce.netebayinkblog.comebayinternetsalestax.com -ebayit.comebayjewelry.com ebayjob.com -ebayla.orgebaylisting.com ebaylocal.netebaylocationsdevacances.com ebaymag.comebaymainstreet.com ebaymall.comebaymarketplace.net ebaymotors.ca ebaymotors.cn -cnebaymotors.comebaymotors.com.cn -cnebaymotors.orgebaymotorsblog.com ebaynow.com ebaynyc.com -ebayon.com -ebayon.netebayoncampus.com ebayopen.comebayopensource.comebayopensource.netebaypakistan.net ebaypark.com ebayparts.com ebaypedia.cn -cnebaypedia.com.cn -cnebayprivacycenter.com -ebayqq.com ebayradio.com ebayrtm.comebayseller.comebayshoesstore.com ebayshop.comebayshop111.comebayshopping.cn -cnebayshopping.com.cn -cnebayshopping.orgebaysocial.com ebaysocial.ru ebaysoho.com ebaysohos.com ebaystatic.cn -cnebaystatic.com ebaystore.comebaystore77.com ebaystores.cn -cn ebaystyle.comebaysweden.com  ebayt.comebaytechblog.comebaytopratedseller.netebaytrading.comebaytradingassistant.com -ebaytv.org ebayuae.netebayvakantiehuizen.com ebayvalet.comebayvietnam.netebayworlds.com  ebayy.com edisebay.com  eebay.com epinions.comeu-consumer-empowerment.comexpertmaker.comfairmarket.comfragrancebay.comfrancemail.com half.com half.com.cn -cn half.tvhalfcanada.com halfjapan.comhandbagsoutletebay.com  iebay.com irribay.comitsbetterwhenyouwinit.com liketwice.comliveauction.com milofetch.com musicbay.netmyconstructionworld.net -myebay.com  nebay.net paisapay.cc paisapay.info paisapay.tv premobay.comprivatemarketplaces.netprivatemarketplaces.us prostores.cn -cn prostores.comprostores.com.cn -cn rethink.net shopibay.net shoping.comsourcingforebay.com.cnsourcingforebay.netsourcingforebay.tv speybay.comstoresense.com -svpply.com telebay.com telesell.com texttobuy.orgtheebayshop.comtheopportunityproject.orgtowerauction.com  vendu.com weareebay.comwatch-ebay.org wwwdecide.com wwwebay.com wwwebay.net wwwwebay.com xindelu.comxn--3et96bj49ahpq.comxn--4vq475g.comxn--4vq477m.comxn--7hv594h.comxn--7hvy28f.cn -cnxn--hb4aw0g.comxn--q41am8x.comxn--qoq462m.comxn--tkry91n.comxn--ubt498knmf.comxn--xsq421m.comxn--xsq605n.comxn--xsq959n.comxn--yf1at58a.com  xxbay.com  yibei.orggittigidiyor.netgittigidiyorsikayet.com -gumtree.sg  jijiji.ca kiji.ca kijii.ca kijij.ca  kijiji.ca kijijiauto.cakijijiautos.ca kijijidns.cn -cnkijijidns.com.cn -cnkijijiforbusiness.ca -kijijii.ca kijijiraps.ca kijji.ca -kijjiji.ca automobile.fr  mobile.demobileinternational.com&mobileinternational.com.cn -cn -q10.jp qoo10.jpachievementanalytics.comcdn-terapeak.comlistinganalytics.netmarketexecutive.net tearapeak.com terapeack.com terapeak.ca terapeak.cn -cn terapeak.comterapeak.com.hk terapeak.hk terapeak.info teraperk.com terrapeak.com thebayuk.com -nexcat.com nexpart.com -nexpart.tv renchead.comwhisolutions.com wrenchead.com-)ebay-25-assets.s3-us-west-1.amazonaws.com - -TUMBLR -tumblr.com - - ZOOMINGIN zoomingin.tv -L -JSDELIVR jsdelivr.net jsdelivr.comcdn.jsdelivr.net -cn - -THELINUXFOUNDATION -kernel.org  linux.org amp.devampproject.comampproject.netampproject.orgcdn.ampproject.org -cnamp.akamaized.netelectronjs.org -jquery.comjquerymobile.com jqueryui.com mochajs.org -nodejs.org openjsf.orglfai.foundationlinuxfoundation.org - - -APPLEDAILYappledaily.comappledaily.com.hkappledaily.com.tw appledaily.hkapplefruity.comapplehealth.com.hk -atnext.combestmallawards.com deluxe.com.hk eracom.com.tw next.hknextdigital.com.hknextdigital.com.twnextfilm.com.hknextmag.com.tw nextmedia.comnextmedia.com.tw nextmgz.comnextplus.com.hk nexttv.com.twnextwork.com.hknextwork.com.tw nextwork.hk nextwork.tw nxtdig.com.hk nxtdig.com.tw omoplanet.com privilege.hk privilege.tw sharpdaily.tw tomonews.nettwnextdigital.com -! -FREENETfreenetproject.org - -TRIBLER tribler.org -( -REDIS redis.io redislabs.com -< - YYETS-ADS cvnad.com -ads -iyoowi.com -ads -& - CHINADECODINGchinadecoding.com - -DELL -cfblob.comcloudchoose.comcloudvolumes.com -cmpaas.com -cpedge.comcrit-staging.comexperience-vmware.com gemfire.netgeodesummit.com getboxer.com gitstar.com gitstar.net gopivotal.net greenplum.net hwslabs.com immidio.comitfromtheinside.comlatamvmwareforum.compivotalinitiative.comsnapvolumes.com ssdevrd.com vcanedge.com vcloudair.net vfabric.netvidmpreview.com -virsto.com -virsto.net vmglobal.net -vmmark.comvmtestdrive.com vmw.comvmware-cloudmanagement.comvmware-techcenter.comvmware20mosaic.comvmwareausnews.com!vmwarecertificationvideos.comvmwarecloud.comvmwaredemandcenter.comvmwareemeablog.comvmwaregrid.comvmwarehorizon.comvmwareidentity.comvmwarelearning.comvmwarelearningplatform.comvmwarestuff.comvmwareusergroupstore.comvmwareviewpoint.com vmwarevmc.com vmwdemo.com vmwgcomms.comvmwhorizonair.comvmwidm-ads.com -vmwidm.comvmwlabconnect.comvmwlearningplatform.com vmworld.comvmworld2010.comvmworld2013.comvmwsalesrewards.comvmwservices.com -vnware.netwhychoosehorizon.comwhychooseview.comwhychoosevmwareeuc.comwindowsuem.comworkspaceair.com xlstudio.comavinetworks.com -badaas.com bitnami.combitnamiapp.combitnamistudio.comdockerizer.comcloudcredibility.comcloudhealthtech.com cloudlive.comkubeacademy.com kubeapps.com -nyansa.com velocloud.com gopivotal.com heptio.cn -cn -heptio.com aliveipc.com aliveitsm.comaliveprofiler.com alivercm.comaliverewind.com alivertsm.com alivevm.com alivevmax.com alivevue.com vsphere.com vsphere.net air-watch.comairwatchexpress.comairwatchqa.com -getws1.com getwsone.comwhatisairwatch.comwhatisworkspaceone.comworkspaceone.comvmware.tt.omtrdc.netdelldell-brand.com dell.com dellcdn.com -W -HAINANAIRLINEShainanairlines.com hnacargo.com  hnair.com 10044.cn -# -MEGA -mega.co.nz mega.nz - -CYTHON -cython.org -( -UCLOUD  ucloud.cn ufileos.com - -YANDE yande.re - -CISCO webex.ca webex.co.in webex.co.it webex.co.jp webex.co.kr webex.co.nz webex.co.uk  webex.com webex.com.au webex.com.br webex.com.cn -cn webex.com.hk webex.com.mx webex.de webex.es webex.fr ciscoacademynetriders.comcciernslabs.comcciesecuritylabs.comccievoicelabs.com  ccna5.netcisco-returns.comcisco-warrantyfinder.comcisco.cn -cn  cisco.com cisco.com.cn -cn -cisco.mobiciscoccservice.comciscoconnectcloud.comciscoconnectcloud.netciscoconnectcloud.orgciscocontest.comciscoerate.com ciscofax.comciscoinvestments.comciscojabbervideo.netciscokinetic.comciscoknowledgenetwork.comciscolearningsociety.orgciscolearningsystem.com ciscolive.comcisconetspace.comcisconetspace.infocisconetspace.netcisconetapp.comciscopartnermarketing.comciscopowercube.comciscoprice.comciscoresearch.comciscosoftware.com ciscospark.caciscospark.com ciscospark.jpciscotaccc.com ciscotr.com ciscoturk.netciscovideo.comciscowebseminars.com cloudlock.com cloupia.com cloupia.netconnect-in-canada.comcoreoptics.net cs.co -devm2m.com dnaspaces.io flipshare.comgpstheseries.com inlethd.cominternetofeverything.com ioe.com kscisco.com m2m.com -mcisco.commediafiles-cisco.com metacloud.com mindmeld.commultiplydiversity.commyciscobenefits.com  mysdn.com -mysdn.info  mysdn.net netacad.com netacad.netnetwork-auth.cn -cnobservable.net obsrvbl.com parstream.com parstream.net parstream.orgservices-exchange.comstatic-cisco.comtandberg-china.com tandberg.com -versly.comvideochampion.com virtuata.comwballiance.com!whyiwantciscotelepresence.com wifi-mx.comyoutubecisco.comapp-dynamics.comappdynamics.co.ukappdynamics.comappdynamics.deappdynamics.frappdynamics.infoappdynamics.jpappdynamics.org -mearki.com meraki-go.com  meraki.hk merakigo.com -tail-f.com  tailf.comcisco.evergage.comciscolivehls-i.akamaihd.net - -JAVBUS -javbus.com -* -JAVWIDE -javwide.tv javwide.com -3 -ENTRUST entrust.netentrustdatacard.com -= -SIGNAL -signal.art -signal.orgsignalusers.org -? -STRIPE -stripe.comstripe.network stripecdn.com - -TUBE8  tube8.com - - -BRIGHTCOVE videojs.com bcove.video bcovlive.io bcvp0rtal.com boltdns.netbrightcove.combrightcove.netbrightcove.servicesbrightcovecdn.combcovlive-a.akamaihd.netbcsecure01-a.akamaihd.netbrightcove.imgix.net -$ -CSDN csdn.net -csdnimg.cn - -OPPO coloros.com finzfin.com -heytap.com heytapcs.comheytapdownload.comheytapimage.com -myoppo.com nearme.com.cn oppo.cn oppo.com  oppo.mobioppodigital.com  oppoer.meoppomobile.com oppopay.com opposhop.cn -realme.com realmebbs.comrealmemobile.com -9 - YCOMBINATORstartupschool.orgycombinator.com -a -LE -le.com lecloud.com -lemall.com letv.com letvcdn.com letvimg.com - -NETFLAV netflav.com - -ONEDRIVE 1drv.com 1drv.mslivefilestore.com onedrive.coonedrive.co.uk onedrive.com onedrive.eu onedrive.net onedrive.orgonedrive.live.comstorage.live.com - - ALJAZEERA -ajplus.net aljazeera.com aljazeera.netpalestineremix.comsarajevopodopsadom.comsrebrenica360.com syhacked.com -g -SPRINGER -apress.com springer.comspringerlink.comspringernature.com wkap.nl -1 -HOMEBREW brew.shhomebrew.bintray.com -^ -SWIFTappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cn - -BITTREX bittrex.com -, -CYLINK -cylink.procylink0122.icu - -GNU gnu.org -7 - FONTAWESOMEfontawesome.comfortawesome.com -` -GEMFURY  fury.blog fury.co fury.dev  fury.help fury.io gemfury.com -1 - LIBERAPAY liberapay.com liberapay.org - -SECTIGO sectigo.comenterprisessl.comhackerguardian.cominstantssl.comoptimumssl.compositivessl.com - -ADDTOANY addtoany.com - -APACHE -apache.org -B -DISQUS -disqus.com disquscdn.comdisqusservice.com - - PINTEREST -pin.it -pinimg.com pinterest.at pinterest.be pinterest.ca pinterest.ch pinterest.cl pinterest.copinterest.co.atpinterest.co.inpinterest.co.krpinterest.co.nzpinterest.co.uk pinterest.compinterest.com.aupinterest.com.bopinterest.com.ecpinterest.com.mxpinterest.com.pepinterest.com.pypinterest.com.uypinterest.com.vn pinterest.de pinterest.dk pinterest.ecpinterest.engineering pinterest.es pinterest.fr pinterest.hu pinterest.id pinterest.ie pinterest.inpinterest.info pinterest.it pinterest.jp pinterest.kr pinterest.mx pinterest.nl pinterest.nz pinterest.pe pinterest.ph pinterest.pt pinterest.ru pinterest.se pinterest.th pinterest.tw pinterest.uk pinterest.vnpinterestmail.com -7 -SCP scp-wiki.net  scpdb.org scpwiki.com - -VGTIME -vgtime.com -D -YY -duowan.com dwstatic.com -yy.com yystatic.com - - -JD3.cn  300hu.com -360buy.com 360buyimg.com -360top.com -7fresh.com baitiao.comblackdragon.com  caiyu.comchinabank.com.cn -dao123.comjcloud-cdn.comjcloud-live.comjcloud-oss.com -jcloud.comjcloudcache.com jcloudcs.com jclouddn.com jcloudec.com jcloudlb.comjcloudlive.com jcloudlv.com jcloudoss.com jcloudss.comjcloudstatic.comjcloudvideo.com  jclps.com -jd-app.com  jd-ex.com jd.cn jd.co.th -jd.com jd.hk jd.id jd.ru jdcache.com jdcloud.com jdcloudcs.comjdcloud-api.comjddapeigou.com jddebug.com jddglobal.com jdjinrong.com  jdpay.com jdpaydns.com jdx.com jdwl.comjingdongjinrong.com -jingxi.com -jkcsjd.com -joybuy.com  joybuy.eslinglonglife.com mayshijia.com minitiao.com  ocwms.com paidaojia.cn -paipai.comprestodb-china.com qianxun.comqingzhouip.com toplife.com -vg.com wangyin.com  wdfok.com yhd.com yihaodian.comyihaodianimg.com yiyaojd.com yizhitou.comads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads - -MINImini4myminicard.comaboutyourmini.comatlantaminidealers.comgreatergothammini.com!midatlanticbmwmotorcycles.commini-abudhabi.commini-antilles.frmini-bahrain.commini-bosnia.com mini-clubs-international.commini-connected.atmini-connected.bemini-connected.chmini-connected.co.ukmini-connected.commini-connected.czmini-connected.demini-connected.dkmini-connected.eemini-connected.esmini-connected.fimini-connected.frmini-connected.iemini-connected.itmini-connected.ltmini-connected.lumini-connected.lvmini-connected.nlmini-connected.nomini-connected.plmini-connected.ptmini-connected.semini-corporate-sales.com mini-coupe.camini-dubai.com -mini-e.commini-egypt.commini-georgia.commini-grouparchiv.demini-grouparchive.commini-jordan.com mini-ksa.commini-kuwait.commini-lebanon.com mini-me.com mini-oman.commini-qatar.commini-srilanka.commini-stjohns.camini-stjohns.commini-tahiti.commini-vietnam.commini-windsor.camini-windsor.com mini.am mini.at mini.be mini.bg mini.by mini.ca mini.cc mini.ch mini.cl -mini.co.cr -mini.co.id -mini.co.il -mini.co.kr -mini.co.me -mini.co.th -mini.co.uk -mini.co.za mini.com mini.com.ar mini.com.bn mini.com.br mini.com.co mini.com.cy mini.com.do mini.com.ec mini.com.gr mini.com.gt mini.com.hr mini.com.mk mini.com.mo mini.com.mt mini.com.mx mini.com.pa mini.com.pe mini.com.ph mini.com.pl mini.com.py mini.com.sg mini.com.tr mini.com.tw mini.com.uy mini.cz mini.de mini.dk mini.dz mini.es mini.fi mini.fr mini.gp mini.hu mini.ie mini.in mini.is mini.it mini.jp mini.kz mini.lu mini.ma mini.md mini.mq mini.mu mini.my mini.nc mini.nl mini.no mini.pt mini.re mini.ro mini.rs mini.ru mini.se mini.si mini.sk mini.tm mini.tn mini.ua -mini101.caminiargentina.comminibrossard.caminibrossard.com minicanada.caminicaribbean.com miniccrc.ca minicooper.caminidealer.comminidealernet.comminidowntown.caminidowntown.com minidrivingexperienceusa.com minidurham.caminidurham.com  minie.com -minifs.comminigeorgian.caminigrandriver.comminihalifax.caminihalifax.com -minihk.comminiinvasion.caminiitalianjob.comminikelowna.caminikelowna.comminilangley.caminilangley.com minilat.com minilaval.ca minilaval.com minilondon.ca minilondon.cominimarkham.caminimarkham.comminimoncton.caminimoncton.comminimontrealcentre.caminimontroyal.caminimontroyal.comminimotoringrewards.comminimotoringschool.comminimotorsport.commininanaimo.camininanaimo.comminioakville.comminiofmonrovia.comminipetfriendly.comminirichmond.caminirichmond.comminisaskatoon.caminisaskatoon.com minishop.ca minispace.comminispecialoffers.caminispygear.comministcatharines.caministeagathe.comministjohns.caminitakesthestates.comminitoronto.caminitroisrivieres.ca miniusa.comminiusaservice.comminiusatires.comminivalueservice.comminivancouver.caminivaughanwest.caminivaughanwest.comminivictoria.caminivictoria.comminivilledequebec.caminivilledequebec.comminiwholesaleconnect.com miniwidget.caminiwindsor.comminiworkshop.comminiyaletown.capugetsoundmini.comshopminiusa.comsouthfloridamini.com -trmini.comtroisrivieresmini.comyours-customised.mini.com - -MONOTYPEfontexplorerx.com  fonts.com  fonts.net fontbook.com fontshop.comfstopimages.com40fontshop-prod-responsive-images.s3.amazonaws.com linotype.com myfonts.com myfonts.net monotype.commonotypeimaging.com bitstream.com itcfonts.com - -SQLITE -sqlite.org - - TENCENT-ADS ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads - -BTCBOX btcbox.co.jp - - - CATEGORY-PORN  1pondo.tv -91porn.com  avgle.com avn.com beeg.com bongacams.com -bcicdn.com bcrncdn.com -btt804.com -btt904.com -bttzyw.com -bttzyw.net t66y.comchaturbate.com e-hentai.org ehgt.org ehtracker.org -ehwiki.org exhentai.orghentaiverse.org eporner.comfanhaodian.com  hegre.com heydoga.com  heyzo.com hqporner.com -javbus.com javfinder.sh findercdn.me o0-2.com  javhd.comjavlibrary.com -javwide.tv javwide.com kindgirls.comlivejasmin.com dditsadn.com dditscdn.com -metart.commetartnetwork.com metcams.comnaiadsystems.com  nsimg.net netflav.com  pigav.com -pornhd.com -phncdn.com phprcdn.com pornhub.compornhubpremium.com  press.vin redtube.com scoreland.comcdn77.scoreuniverse.com spankbang.comtheporndude.com  tube8.comvaginacontest.com  x-art.com xhamster.comxhamsterlive.com  xhcdn.comwidgets.stripst.com#collector.xhamster.com -ads xnxx.com xnxx-cdn.com xvideos.comxvideos-cdn.com youjizz.comyjcontentdelivery.com youporn.com - -CYGWIN -cygwin.com -C -GITBOOK gitbook.com -gitbook.iogitbook.zendesk.com -# - PASSIONTIMESpassiontimes.hk -_ -FEDORA getfedora.orgfedoraforum.orgfedoraproject.orgfedoramagazine.org -8 -TARGET -target -target.comtargetimg1.com -1 -ZIROOM -ziroom.comziroomapartment.com - -YOUTUBE youtubeggpht.cn -cn  ggpht.comgooglevideo.comwithyoutube.com youtu.beyoutube-nocookie.com -youtube.ae -youtube.al -youtube.am -youtube.at -youtube.az -youtube.ba -youtube.be -youtube.bg -youtube.bh -youtube.bo -youtube.by -youtube.ca youtube.cat -youtube.ch -youtube.cl -youtube.co youtube.co.ae youtube.co.at youtube.co.cr youtube.co.hu youtube.co.id youtube.co.il youtube.co.in youtube.co.jp youtube.co.ke youtube.co.kr youtube.co.ma youtube.co.nz youtube.co.th youtube.co.tz youtube.co.ug youtube.co.uk youtube.co.ve youtube.co.za youtube.co.zw youtube.comyoutube.com.aryoutube.com.auyoutube.com.azyoutube.com.bdyoutube.com.bhyoutube.com.boyoutube.com.bryoutube.com.byyoutube.com.coyoutube.com.doyoutube.com.ecyoutube.com.eeyoutube.com.egyoutube.com.esyoutube.com.ghyoutube.com.gryoutube.com.gtyoutube.com.hkyoutube.com.hnyoutube.com.hryoutube.com.jmyoutube.com.joyoutube.com.kwyoutube.com.lbyoutube.com.lvyoutube.com.lyyoutube.com.mkyoutube.com.mtyoutube.com.mxyoutube.com.myyoutube.com.ngyoutube.com.niyoutube.com.omyoutube.com.payoutube.com.peyoutube.com.phyoutube.com.pkyoutube.com.ptyoutube.com.pyyoutube.com.qayoutube.com.royoutube.com.sayoutube.com.sgyoutube.com.svyoutube.com.tnyoutube.com.tryoutube.com.twyoutube.com.uayoutube.com.uyyoutube.com.ve -youtube.cr -youtube.cz -youtube.de -youtube.dk -youtube.ee -youtube.es -youtube.fi -youtube.fr -youtube.ge -youtube.gr -youtube.gt -youtube.hk -youtube.hr -youtube.hu -youtube.ie -youtube.in -youtube.iq -youtube.is -youtube.it -youtube.jo -youtube.jp -youtube.kr -youtube.kz -youtube.la -youtube.lk -youtube.lt -youtube.lu -youtube.lv -youtube.ly -youtube.ma -youtube.md -youtube.me -youtube.mk -youtube.mn -youtube.mx -youtube.my -youtube.ng -youtube.ni -youtube.nl -youtube.no -youtube.pa -youtube.pe -youtube.ph -youtube.pk -youtube.pl -youtube.pr -youtube.pt -youtube.qa -youtube.ro -youtube.rs -youtube.ru -youtube.sa -youtube.se -youtube.sg -youtube.si -youtube.sk -youtube.sn youtube.soy -youtube.sv -youtube.tn -youtube.tv -youtube.ua -youtube.ug -youtube.uy -youtube.vnyoutubeeducation.comyoutubegaming.comyoutubego.co.idyoutubego.co.in youtubego.comyoutubego.com.br youtubego.id youtubego.inyoutubei.googleapis.comyoutubekids.comyoutubemobilesupport.com yt.be  ytimg.com -& - BAICAONETWORKbaicaonetwork.com -4 -CSIS csis.orgcsis-prod.s3.amazonaws.com -, -MATTERS matters.news matters.one -g - READTHEDOCSreadthedocs-hosted.comreadthedocs.comreadthedocs.ioreadthedocs.org - -SINA-ADS 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -ads -) -LINGUEE linguee.com  deepl.com -" - MOCLOUDPLUSmocloudplus.com -O -PUBMATIC pubmatic.compubmatic.co.jpads.pubmatic.com -ads - - WORDPRESSvideopress.com w.org wordpress.com wordpress.org wordpress.tv wp-themes.com -wp.com  wpvip.com -7 - BUYMEACOFFEE buymeacoff.eebuymeacoffee.com -9 -EMBEDLY embed.ly embedly.com playerjs.io - - INOREADER inoreader.com -' -POLYV  polyv.net videocc.net - -UMENG mobmore.com  umeng.comumengcloud.com  umsns.com umtrack.com -uyunad.comwhalecloud.comalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads - - ZHIHUISHU zhihuishu.com -T -NOW now.com -now.com.hk nowe.com -now-tv.comnow-ashare.com -5 -YOUJIZZ youjizz.comyjcontentdelivery.com -_ -CATEGORY-WIKI-CNhuijistatic.com huijiwiki.com moegirl.orgmoegirl.org.cn -) -DRIBBBLE drbl.in dribbble.com - -DUREXdewitwithdurex.com dotherex.com durex-shop.chdurex-shopline.comdurex-slovenia.si durex.at durex.be durex.cl durex.co.id durex.co.il durex.co.nz durex.co.th durex.co.uk durex.co.za  durex.com durex.com.au durex.com.bd durex.com.cn -cn durex.com.co durex.com.hr durex.com.my durex.com.ng durex.com.pe durex.com.ph durex.com.pk durex.com.sg durex.com.tr durex.cz durex.de durex.dk durex.ee durex.es durex.fi durex.fr durex.hu durex.ie durex.it durex.jp durex.lv durex.mx durex.nl durex.no durex.pl durex.pt durex.ro durex.ru durex.se durex.us durexcam.comdurexcanada.comdurexchina.comdurexchina.com.cn -cndurexindia.comdurexloveclub.comdurexukraine.com durexusa.comlovesexdurex.cn -cnlovesexdurex.complaybydurex.com shopdurex.com trydurex.net trydurex.tv -H -BTTZYW -btt804.com -btt904.com -bttzyw.com -bttzyw.net - - -SKILLSHAREskillshare.com - -ZDNS zcmbc.com.cn zdns.cn zdns.net.cn zdns.org.cn zdnscloud.biz zdnscloud.cn zdnscloud.comzdnscloud.com.cnzdnscloud.info zdnscloud.netzdnscloud.net.cnzdnscloud.org.cn -0 -BITLY -bit.ly  bitly.com bitly.is - -PCRE pcre.org - -WINDY  windy.com - -ADIDAS  adidas.at  adidas.be  adidas.ch adidas.co.in adidas.co.uk -adidas.com adidas.com.cn -cn adidas.com.sa adidas.com.tw  adidas.cz  adidas.de  adidas.dk  adidas.hu  adidas.ie  adidas.it  adidas.nl  adidas.pl  adidas.ru  adidas.se - -TIKTOK -muscdn.com -musical.ly -tiktok.com tiktokcdn.com tiktokv.com#p16-tiktokcdn-com.akamaized.net - -ZOHO zoho.com zoho.com.au zoho.eu zoho.in zohocdn.comzohomeetups.comzohomerchandise.comzohopublic.comzohoschools.comzohostatic.com zohostatic.inzohouniversity.comzohowebstatic.com - - ALIBABA-ADS a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -ads - - APPLE-DEVappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cncalendarserver.orgapple-cloudkit.com cups.orgdeveloper.apple.comdevimages-cdn.apple.comdevstreaming-cdn.apple.comfoundationdb.orgmacosforge.org macruby.com macruby.net macruby.orgapple-mapkit.complaygrounds-cdn.apple.com carekit.orgresearchandcare.orgresearchkit.cn -cnresearchkit.com.cn -cnresearchkit.hkresearchkit.netresearchkit.orgresearchkit.tvservicetalk.io -webkit.org webkitgtk.org wpewebkit.orgwebobjects.com-)appleworldwidedeveloper.hb-api.omtrdc.net)%appleworldwidedeveloper.sc.omtrdc.net#p-events-delivery.akamaized.net - -TABOOLA taboola.com - -RARBG rarbg.me rarbg.to rarbg.israrbgaccess.org rarbggo.orgrarbgmirror.comrarbgmirror.orgrarbgproxy.org rarbgprx.orgrarbgunblock.com - - -SALESFORCE appcloud.comappexchange.com attic.iobeyondcore.combuddymedia.com chatter.comcloudcraze.com cotweet.com data.comdemandware.com desk.comdocumentforce.comdreamforce.com einstein.comexacttarget.com  force.com govforce.comgravitytank.com heywire.com krux.commarketingcloud.com metamind.io -pardot.com quotable.com radian6.com relateiq.comsalesforce.comsalesforce.orgsalesforceiq.comsalesforceliveagent.com salesforcemarketingcloud.com sequence.comsfdcstatic.com -sforce.com site.com -social.comsteelbrick.com twinprime.comweinvoiceit.com -< -SHOWTIMEANYTIMEshowtimeanytime.com showtime.com - -PSIPHON psiphon3.com - -MYSQL  mysql.com -" -NIST nist.gov time.gov -j - PLAYSTATION playstationplaystation.complaystation.net sonyentertainmentnetwork.com -1 -TIANCITY tiancity.comtiancitycdn.com - -AHMIA ahmia.fi - -GUCCI guccichimeforchange.orggucci.cn -cn  gucci.comguccimuseo.comguccitimeless.comregiongold.com -, -SECOM secom.co.jpsecomtrust.net - -STUNNEL stunnel.org - -SYOSETU syosetu.com - -CLOJURE clojure.org - -MSNmicrosoftnews.ccmicrosoftnews.cn -cnmicrosoftnews.commicrosoftnews.netmicrosoftnews.orgmicrosoftnewsforkids.commicrosoftnewsforkids.netmicrosoftnewsforkids.orgmicrosoftnewskids.commicrosoftnewskids.netmicrosoftnewskids.org msn.commsnewskids.commsnewskids.netmsnewskids.org msnkids.com -U -SB -dns.sb ip.sb loli.net sb.sb sm.msu.nu -www.sb -" - SOUNDOFHOPEsoundofhope.org - -GEEKPARK geekpark.net - -IPFS ipfs.io -2 - NEWRELIC-ADS"js-agent.newrelic.com -ads - -CLOUDN  cloudn.me -% -DEMOCRACYEARTHdemocracy.earth -" - EPICBROWSERepicbrowser.com - -JUPYTER jupyter.org - - NETEASE-ADSanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads - - -QINGTINGFM qingting.fm - - GREATFIRE greatfire.org -* -HKOPENTV hkopentv.com fantv.hk - -JINJIANG  jjwxc.net -N - CLOUDCONE clco.cc -cloudc.one cloudcone.com cloudcone.net - -CONAIR  conair.me - -KNOVEL -knovel.com - -ORIGIN dawngate.com eastore.comlordofultima.com -origin.com  origin.tv#cloudsync-prod.s3.amazonaws.comorigin-a.akamaihd.netrtm.tnt-ea.comssl-lvlt.cdn.ea.com - -SMPTE  smpte.org - -ATTaccbusiness.com -atandt.comatt-bundles.com att-japan.com att-mail.comatt-promotions.com att-rsvp.comatt.cn -cn att.com att.jobs att.net -att.tvattalascom.comattbelieves.comattbusiness.net attcenter.comattcollaborate.com -attcorp.cn -cnattexperts.com attglobal.netattinnovationspace.comattinternetservice.com -attjoy.comattnetclient.comattonlineoffers.com attproxy.comattpublicpolicy.comattpurchasing.comattsavings.comattspecial.com -attssl.comattstadium.comattsuppliers.com atttvnow.comattuverseoffers.comattuverseonline.com attvoip.com attwifi.comattwirelessinternet.comattwirelessonline.comattwirelesssolutions.com bellsouth.net -budatt.com -cobatt.com currently.com currently.netdirectvbusinessmarket.comenterprisepaging.com itcanwait.com mymmode.comnaturalvoices.comnettyinternet.comopenapiplatform.comopenapiservice.compatentgold.netpaygonline.comraponlinereview.com signalbar.comsundayready.com synaptic.netthanksloyalty.com tvpromise.com -uverse.comwebhosting.com att-idns.net -attdns.com -attdns.netna-att-idns.netaudiencenetwork.comaudiencenetwork.tvcabletvdirectv.com dierectv.com -dirctv.com direcpath.com direcpath.netdirecttv-deals.tvdirecttv-dish.com dkrecttv.com directtv.netdirecttvdeals.tvdirecttvreviews.comdirectv-4-you.comdirectv-newyork.com directv.comdirectvadsales.comdirectvatlantaga.comdirectvboston.comdirectvbundles.comdirectvbusiness.comdirectvcincinnatioh.comdirectvcinema.comdirectvconnect.comdirectvcookevilletn.comdirectvcrossvilletn.comdirectvdealer.comdirectvdeals.comdirectvdealsnow.com directvdsl.tvdirectvforhotels.comdirectvgrandslam.comdirectvhouston.comdirectvinternet.comdirectvkentucky.comdirectvlebanontn.comdirectvlosangeles.comdirectvmetropolisil.comdirectvmonitoring.comdirectvmurfreesborotn.comdirectvnewhampshire.comdirectvnow.comdirectvoffercodes.comdirectvonline.comdirectvplans.comdirectvpomise.comdirectvpromise.comdirectvpromotions.comdirectvrebate.comdirectvrichmond.comdirectvsavings.comdirectvsports.comdirectvsundayticket.com direectv.com -diretv.comdtv2009offers.comfreedirecttvspecial.com getdirect.tv hddirectv.com le-direct.tvmydirectgroove.commydirectvchannels.comredzonechannel.com txdirectv.com  xandr.com -Q - BANDWAGONHOSTbandwagonhost.com bwh1.net bwh8.net  bwh88.net -, -BYPASSCENSORSHIPbypasscensorship.org -U - CLEARASIL clearasil.cn -cnclearasil.com.cn -cn clearasil.us - - FASTSTONE faststone.org - -MDNdeveloper.mozilla.org($interactive-examples.mdn.mozilla.netmdn.mozillademos.orgmedia.prod.mdn.mozit.cloud - -FIRESIDE fireside.fm -E - HIIDO-ADSmlog.hiido.com -adsylog.hiido.com -ads -H - NEXITALLY nexitally.comnexitallysafe.com nexitcore.com -` -OAN aweencore.com  awetv.comherringnetwork.com oanencore.com oann.com - -XBOXcallersbane.com minecraft.netminecraftshop.com -mojang.comforzamotorsport.netforzaracingchampionship.com forzarc.comorithegame.comrenovacionxboxlive.comtellmewhygame.com xbox.co xbox.com xbox.eu xbox.org -xbox360.co xbox360.com -xbox360.eu xbox360.org -xboxab.comxboxgamepass.comxboxgamestudios.com xboxlive.cn -cn xboxlive.com -xboxone.co xboxone.com -xboxone.euxboxplayanywhere.comxboxstudios.com - -ERLANG -erlang.org - - FFPROFILE ffprofile.com - -FREEBSD freebsd.org -L -APKPURE apkpure.com cdnpure.com pureapk.com -winudf.com -' -CODING  coding.me -coding.net -@ - ZUOYEBANG zuoyebang.cc zuoyebang.com -zybang.com -1 - STACKPATH stackpath.com stackpath.dev - -TIDELIFT tidelift.com - -HEGRE  hegre.com -N -SEASUNseasungames.com xishanju.com xoyo.com testplus.cn - -XPOSED xposed.info - - 58TONGCHENG -58.com  58.com.cn 5858.com  58che.com 58xueche.com -anjuke.comanjukestatic.com chinahr.com -daojia.com diandao.org fang.com  sofun.com soufunimg.com  jxedt.com zhuancorp.comzhuanspirit.comzhuanzhuan.com -7 -DART dart.dev dartlang.org dartpad.dev - - RPMFUSION rpmfusion.org - -ISGD is.gdv.gd - -MOCHA mochajs.org -G -TEX ctan.orglatex-project.org -miktex.org tug.org - -TINYURL tinyurl.com - -EGGHEAD -egghead.io - -ELECTRONelectronjs.org - - FACEBOOK-DEVaccountkit.comatscaleconference.com botorch.org -buck.build buckbuild.comcomponentkit.org draftjs.org -f8.comfacebookappcenter.infofacebookappcenter.netfacebookappcenter.orgfacebookconnect.comfacebookdevelopergarage.comfaciometrics.com fasttext.cc fbf8.com fbinfer.com fblitho.com fbredex.com -fbrell.com flow.dev flow.org flowtype.org frescolib.org hacklang.org hhvm.commakeitopen.com mcrouter.net mcrouter.orgmessengerdevelopers.com -ogp.meopengraphprotocol.comopengraphprotocol.org  parse.com pyrobot.org  react.com reactjs.com reactjs.org recoiljs.org rocksdb.com rocksdb.net rocksdb.orgyogalayout.comdevelopers.facebook.com - - MEADJOHNSONenfinitas.com.cn -cnmeadjohnson.commeadjohnson.com.cn -cnmeadjohnson.com.hkmeadjohnson.com.twmeadjohnson.net - -WHATSAPP wa.mewhatsapp-plus.infowhatsapp-plus.mewhatsapp-plus.net whatsapp.cc whatsapp.com whatsapp.info whatsapp.net whatsapp.org whatsapp.tvwhatsappbrand.com -, -ACPLAY -acplay.netdandanplay.com - - BRIDGESTONE bridgestone  bandag.cc -bandag.comblizzak-juken.jpbookmybridgestonetyre.combridgestone-asiapacific.combridgestone-bandag.combridgestone-brand.com#bridgestone-business-service.jpbridgestone-korea.co.krbridgestone-plt-eng.combridgestone-tac-oman.combridgestone.clbridgestone.cn -cnbridgestone.co.crbridgestone.co.idbridgestone.co.inbridgestone.co.jpbridgestone.co.thbridgestone.combridgestone.com.arbridgestone.com.brbridgestone.com.cn -cnbridgestone.com.cobridgestone.com.mxbridgestone.com.sgbridgestone.com.twbridgestone.com.vnbridgestoneamericas.combridgestonearena.combridgestonecntc.combridgestonecomercial.clbridgestonecomercial.co.crbridgestonecomercial.com.arbridgestonecomercial.com.brbridgestonecomercial.com.cobridgestonecomercial.com.mxbridgestonegolf.combridgestonegz.combridgestonela.combridgestonemarketing.combridgestonemerchandise.com bridgestonenationalfleet.combridgestoneperformance.com#bridgestoneperformancemedia.net bridgestonerapiddelivery.combridgestonerewards.combridgestonesyb.combridgestonetire.cabridgestonetire.combridgestonetyre.com.mybridgestonevan.combridgestonewx.combs-adreview.com bs-awh.ne.jp  bsw.co.jp -bsw.jpcarcare-and-tireshop.jpdaytontrucktires.com ecoforme.jp gcrtires.comjsbridgestone.commenshin-channel.com monitrix.net mr-tireman.jpmybridgestoneeducation.com playz.jp -potenza.jpprodrive-japan.comstore-bridgestonesports.com thetread.com tiresplus.com cfna.combastropfirestone.comcrossfitfirestone.com firestone.clfirestone.co.crfirestone.com.arfirestone.com.brfirestone.com.cofirestone.com.mxfirestonebpco.comfirestonecomercial.clfirestonecomercial.co.crfirestonecomercial.com.arfirestonecomercial.com.brfirestonecomercial.com.cofirestonecomercial.com.mx!firestonecompleteautocare.comfirestonedrivestore.comfirestoneip.comfirestonerewards.comfirestonetire.cafirestonetire.comhibdontire.comwheelworks.netnokiantyres.cn -cnnokiantyres.com - -CANON canon c-ij.com canon-cee.comcanon-cmos-sensors.com canon-cna.comcanon-ebm.com.hkcanon-emea.comcanon-emirates.aecanon-europa.comcanon-europe.com canon-me.com canon-ois.qacanon-se.com.tw canon.am canon.at canon.az canon.ba canon.be canon.bg canon.ca canon.chcanon.cn -cn canon.co.il canon.co.uk canon.co.za  canon.com canon.com.al canon.com.au canon.com.by canon.com.cn -cn canon.com.cy canon.com.hk canon.com.mk canon.com.mt canon.com.my canon.com.tr canon.com.tw canon.cz canon.de canon.dk canon.ee canon.es canon.fi canon.fr canon.ge canon.gr canon.hr canon.hu canon.ie canon.it canon.kz canon.lt canon.lu canon.lv canon.me  canon.net canon.nl canon.no canon.pl canon.pt canon.ro canon.rs canon.ru canon.se canon.si canon.sk canon.tj canon.ua canon.uzcanonfoundation.orgcanonproprinters.com -* -JIANSHU jianshu.com -jianshu.io - -MOZILLA firefox.comdeveloper.mozilla.org($interactive-examples.mdn.mozilla.netmdn.mozillademos.orgmedia.prod.mdn.mozit.cloud rust-lang.org  rustup.rs  crates.io docs.rsmozilla.community mozilla.net mozilla.org bugzilla.org getpocket.comseamonkey-project.orgthunderbird.net - -XDTY xdty.org -3 - -CONTENTFULcontentful.com ctfassets.net - -GOFUNDME gofundme.com - -JENKINS -jenkins.io -. -U17 u17.com u17i.com u17t.com -- - ACFUN-ADS apilog-web.acfun.cn -ads -I -MADSHI -madshi.net  madvr.com  madvr.net madvrlabs.llc -p -ONEPLUS oneplus.com -oneplus.cn oneplus.net opstatics.comoneplusbbs.com h2os.com - -SPACEX -spacex.com - -YAHOO -flickr -flickr.com -flickr.netstaticflickr.com ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -ads yahoo yahoo.ae yahoo.am yahoo.as yahoo.at yahoo.az yahoo.ba yahoo.be yahoo.bg yahoo.bi yahoo.bs yahoo.bt yahoo.by yahoo.ca  yahoo.cat yahoo.cd yahoo.cg yahoo.ch yahoo.cl yahoo.cmyahoo.cn -cn yahoo.co.ao yahoo.co.bw yahoo.co.ck yahoo.co.cr yahoo.co.id yahoo.co.il yahoo.co.in yahoo.co.jp yahoo.co.kr yahoo.co.mz yahoo.co.nz yahoo.co.th yahoo.co.tz yahoo.co.uk yahoo.co.uz yahoo.co.ve yahoo.co.vi yahoo.co.za  yahoo.com yahoo.com.af yahoo.com.ag yahoo.com.ai yahoo.com.ar yahoo.com.au yahoo.com.bd yahoo.com.bo yahoo.com.br yahoo.com.bz yahoo.com.cn -cn yahoo.com.co yahoo.com.do yahoo.com.ec yahoo.com.eg yahoo.com.es yahoo.com.fj yahoo.com.gi yahoo.com.gt yahoo.com.hk yahoo.com.kw yahoo.com.lb yahoo.com.ly yahoo.com.mt yahoo.com.mx yahoo.com.my yahoo.com.na yahoo.com.nf yahoo.com.om yahoo.com.pa yahoo.com.pe yahoo.com.ph yahoo.com.pk yahoo.com.pr yahoo.com.py yahoo.com.sa yahoo.com.sb yahoo.com.sg yahoo.com.sv yahoo.com.tj yahoo.com.tr yahoo.com.tw yahoo.com.ua yahoo.com.uy yahoo.com.vc yahoo.com.vn yahoo.cz yahoo.de yahoo.dj yahoo.dk yahoo.dm yahoo.ee yahoo.es yahoo.fi yahoo.fm yahoo.fr yahoo.ge yahoo.gg yahoo.gl yahoo.gm yahoo.gp yahoo.gr yahoo.gy yahoo.hk yahoo.hr yahoo.hu yahoo.ie yahoo.im yahoo.in yahoo.is yahoo.it yahoo.je yahoo.jo yahoo.la yahoo.lt yahoo.lu yahoo.lv yahoo.md yahoo.me yahoo.mk yahoo.mw yahoo.mx  yahoo.net yahoo.nl yahoo.no yahoo.nu yahoo.ph yahoo.pl yahoo.pn yahoo.ps yahoo.pt yahoo.ro yahoo.ru yahoo.rw yahoo.se yahoo.sg yahoo.sh yahoo.si yahoo.sk yahoo.sm yahoo.sn yahoo.so yahoo.sr yahoo.st yahoo.tg yahoo.tk yahoo.tl yahoo.tm yahoo.tn yahoo.vg yahoo.ws maktoob.com -myguide.hkyahoo-news.com.hk yahooapis.com yahoodns.netyahoofinance.comyahoohealth.comyahoomusic.comyahoosportsbook.com yho.com yimg.com  ymail.com - - 4PARADIGM 4paradigm.com -R -ARPHIC -arphic.com arphic.com.cn arphic.com.twifontcloud.com -< -NGA -nga.cn 178.com -ngabbs.com ngacn.cc - - -HONGKONGFPhongkongfp.com -@ -JQUERY -jquery.comjquerymobile.com jqueryui.com - -MIRAHEZE miraheze.org -S - OGURY-ADS ogury.com -ads -presage.io -adsogury.co -ads -# - PRIVACYTOOLSprivacytools.io -C -ADDTHIS addthis.comaddthiscdn.comaddthisedge.com - - BITWARDEN bitwarden.com -@ - GLOBALVOICESglobalvoices.orgglobalvoicesonline.org -9 -SHADOWSOCKSCOMshadowsocks.comshadowsocks.nl - -SKYsky sky.com skyassets.comskyoceanrescue.comskyoceanrescue.deskyoceanrescue.it skysports.com skysports.fr skysports.ieskysportsonline.comskysportsracing.com -" - SUBLIMETEXTsublimetext.com - -0X0 -0x0.st -j -BAHAMUT gamer.com.twbahamut.com.twgamer-cds.cdn.hinet.netgamer2-cds.cdn.hinet.net - -IETF ietf.org - -I2P -geti2p.net - -PINGPE ping.pe - -RFA rfa.org - -W3C -w3.org - -TELEGRAM contest.com  graph.orgquiz.directoryt.me tdesktop.com -telega.one -telegra.ph telegram.dog telegram.me telegram.orgtelegram.space -telesco.pe -tg.dev tx.meusercontent.dev -Y -UNIONPAY  95516.com unionpay.comunionpayintl.comunionpaysecure.com -] -BUYPASSbuypass-ssl.com buypass.com -buypass.no -buypass.se minid.no - - CHINAMOBILE 10086.cn 139.comchinamobile.comchinamobileltd.com migucloud.com migu.cn -cmvideo.cn miguvideo.com andfx.cn  andfx.net -cmicrwx.cncmpassport.comfetion-portal.com fetionpic.com mmarket.com mmarket6.com -N -CNBC cnbc.com -cnbcfm.com -nbcuni.comnbcudigitaladops.com - -YOUPORN youporn.com -@ -BETHESDA bethesda.net doom.comelderscrolls.com -J -FLICKR -flickr -flickr.com -flickr.netstaticflickr.com - - -INFRAPEDIAinfrapedia.com -F -DEEPIN  deepin.cn -deepin.com  deepin.io -deepin.org -v -NATIONALGEOGRAPHICnatgeomaps.comnationalgeographic.com"nationalgeographicpartners.com ngeo.com -t - CAMBRIDGE cambridge.org"cambridgeschoolshakespeare.comcambridgedigital.netcambridgemaths.org -h -CMB cignacmb.comcmb-leasing.com cmbchina.com cmbi.com.hkcmbwinglungbank.com - -DEDAO biji.com -ddmogo.com -igetget.cn igetget.com luojilab.com  mogoo.com -sao.cn saolife.com - -VOXMEDIA cstatic.net -curbed.com  eater.comfunnyordie.com meridian.net polygon.com -racked.com -recode.net sbnation.com theverge.com vox-cdn.com vox.comvoxcreative.comvoxfieldguide.com voxmedia.com -voxops.net -G -ASAHI  asahi.com asahicom.jpasahishimbun.sc.omtrdc.net -y -GANNETTgannett-cdn.com gannett.comgannettdigital.com usatoday.comusatodaynetworkservice.com -B -HASKELL haskell.orghaskellstack.org stackage.org - -DROPBOX db.ttdropbox-dns.com dropbox.com dropbox.techdropboxapi.comdropboxbusiness.comdropboxcaptcha.comdropboxforum.comdropboxforums.comdropboxinsiders.comdropboxmail.comdropboxpartners.comdropboxstatic.comdropboxusercontent.comgetdropbox.com&"paper-attachments.s3.amazonaws.com - -LINUX  linux.org -% -MIHOYO -mihoyo.com bh3.com - -MUCINEX -mucinex.ca -mucinex.cn mucinex.co.nz mucinex.commucinex.com.cn mucinex.net -mucinex.tvmucinexprofessional.com -; -UBERuber-assets.com uber.com ubereats.com - -BRAVEUXbraventures.com braveux.com -compass.isfightforux.comforthebadge.comstockagainstphotography.com -{ - CLARIVATEnewisiknowledge.comisiknowledge.com clarivate.comwebofknowledge.comwebofscience.com -( -DEBIAN -debian.org -dotdeb.org - - WIRESHARK wireshark.org - -TUNSAFE tunsafe.com -& - VAGINACONTESTvaginacontest.com -$ -GFW  gfw.press -gfw.report -# -QT qt.ioqt-project.org -& - STEAMUNLOCKEDsteamunlocked.net -6 - RUBYCHINAruby-china.orgrubyconfchina.org -+ -XIMALAYA ximalaya.com  xmcdn.com - -BILIBILI -acg.tv acgvideo.com -b23.tv biliapi.com biliapi.net bilibili.combilibiligame.net biligame.com biligame.net bilivideo.com  hdslb.com im9.com -mincdn.com$ upos-hz-mirrorakam.akamaized.net - -DIGICERT digicert.com freessl.com geotrust.com rapidssl.com -thawte.com  thawte.de  thawte.fr - -HP -123-hp.com123hplaserjet.com8008206616.com as-hp.cabaterias-hp.com campushp.comcolorprotechnology.com -compaq.orgdavid-laserscanner.com demoprint.comdigitalpack.com eprintsw.comgaming-notebooks.comgetprintersupports.comhp-imagine.comhp-infolab.com hp-invent.comhp-invent.info -hp-mns.comhp-printing.comhp-webplatform.com  hp-ww.com -hp.com -hp.company hp.io hp3d.comhp3dmetals.comhp3dprinter.comhp3dsamplepromo.comhpallinoneprinter.comhpapplicationscenter.com hpbestbuy.comhpbluecarpet.comhpbluecarpet.nethpbuiltforlearning.com hpbundle.com hpcampus.comhpcatridge.com  hpccp.com  hpceo.com  hpcmw.nethpcodewarsbcn.comhpcomputers.comhpcomputerservices.comhpconnected.comhpconnected.nethpconnected.orghpconnected.ushpconnectedstage.comhpcontinuum.com  hpcpi.com hpcu.orghpcustomersupport.comhpcustomersupport.net -hpdaas.comhpdesignjetl25500.comhpdesktopcomputer.comhpdownloadstore.comhpdreamcolor.com hpdriver.com hpdrivers.com hpdrivers.org hpeclipse.com hpengage.com hpeprint.comhpeprintcenter.com hpeurope.comhpeventcenter.com -hpfaqs.comhpfeedback.com -hpgift.com -hpgpas.comhpgraphicspartners.com  hpiie.org -hpinc.info  hpinc.nethpindigopress.com hpinkjet.comhpinstantink.cahpinstantink.comhpkeyboard.com hplaptop.comhplaptopbattery.comhplargeformatremote.comhplatexknowledgecenter.com hplfmedia.com  hplip.nethplipopensource.comhpmarketplace.comhpmemorychips.comhpmicrcartridge.com -hpmini.com hpmobile.comhpofficejetprinter.comhponlinehelp.comhponlineprinting.comhppage5000.comhppavillionlaptop.comhpphotoscanners.comhpprinterdrivers.comhpprinterinstalls.comhpprintersupplies.comhpsalescentral.com hpserver.comhpservices.comhpshooping.com -hpshop.com hpshoping.com hpshopping.hk hpsignage.com hpsmart.com hpsmarts.comhpsmartstage.comhpsmartupdate.comhpsoftware.com hpspeaker.comhpsprocket.com  hpsso.comhpstore-china.comhpstore-emea.com -hpstore.cn -cn hpstore.comhpstorethailand.com hpsupport.comhpsuresupply.comhptechventures.com hpto.net hptouch.comhptouchpointmanager.comhptrainingcenter.com  hpuae.comhpuniversity.infohpusertraining.com -hpveer.comhpvirtualthin.com hpwallart.comhpwellnesscentral.com  hpwsn.com i-123-hp.com inkchoice.cominstantink.comleavinghpinc.comlife-global.orgmyhpsupport.com printeron.comprintersetupsupport.comprintspots.comprintspots.netserviceshp.comthecompaqstore.comtouchsmartpc.comtouchsmartpc.nettouchsmartpc.orgworkstations.tv www8-hp.comxn--6eup7j.comxn--6eup7j.nethpstore.corpmerchandise.com - -AVN avn.com -& -HUYA huya.com msstatic.com - - IQIYI-ADSa.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -ads - -ARCHIVE archive.org -u - -ATTWATCHTV -att.tv atttvnow.comattwatchtv.com directv.com  dtvce.comnettyinternet.com -k -DOUYU  douyu.com douyu.tv douyucdn.cn douyucdn2.cn douyuscdn.com douyutv.com -- -BITFLYER bitflyer.com bitflyer.jp -* - -FLURRY-ADSdata.flurry.com -ads - - -POSTGRESQLpostgresql.org -( -LTN -ltn.com.tw yes123.com.tw -? -MAFENGWO mafengwo.cn mafengwo.com mafengwo.net -/ -MAPBOX mapbox.cn -cn -mapbox.com - -OUPamamanualofstyle.com anb.org blackstonespoliceservice.com epigeum.com oed.com  omscr.com oup.com -ouplaw.com ox.ac.ukoxfordaasc.comoxfordartonline.comoxfordbibliographies.comoxfordclinicalpsych.com oxforddnb.comoxfordfirstsource.comoxfordhandbooks.comoxfordlawtrove.comoxfordmedicine.comoxfordmusiconline.comoxfordpoliticstrove.com oxfordre.comoxfordreference.comoxfordscholarlyeditions.comoxfordscholarship.comoxfordwesternmusic.comsipriyearbook.org ukwhoswho.com"universitypressscholarship.comveryshortintroductions.com -b - YAHOO-ADS ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -ads -6 -AGORA agora.io -agoraio.cn agoralab.co - -AMAZON amazon-adsystem.com -adswholecitiesfoundation.orgwholefoodsmarket.comwholekidsfoundation.orgwholeplanetfoundation.orgaws -amazonimdb -kindle xn--cckwcxetdxn--jlq480n2rg yamaxun -zapposaboutamazon.cn -cnaboutamazon.co.ukaboutamazon.comaboutamazon.com.auaboutamazon.deaboutamazon.esaboutamazon.euaboutamazon.fraboutamazon.inaboutamazon.itaboutamazon.jpaboutamazon.pl alexafund.cn -cnalexafund.com.cn -cn amaaozn.comamazon-fashions.comamazon-jp-recruiting.comamazon-lantern.comamazon-launchpad.com  amazon.ae  amazon.ca amazon.cn -cn amazon.co.jp amazon.co.uk -amazon.com amazon.com.au amazon.com.br amazon.com.mx amazon.com.tr  amazon.de  amazon.es  amazon.fr  amazon.in  amazon.it amazon.jobs  amazon.jp  amazon.nl -amazon.red  amazon.sgamazonalexavoxcon.comamazonauthorinsights.comamazonbusiness.cn -cn!amazonbusiness.com.cn -cnamazonbusiness.orgamazonbusinessblog.comamazonchoice.cn -cnamazonchoice.com.cn -cnamazonchoices.cn -cn amazonchoices.com.cn -cnamazondevicesupport.comamazonfctours.comamazonianblog.comamazonimages.comamazoninspire.cn -cn amazoninspire.com.cn -cnamazonlaunchpad.cn -cnamazonlaunchpad.com"amazonlaunchpad.com.cn -cn amazonlending.com.cn -cn!amazonliterarypartnership.comamazonlumberyard.wang amazonnow.cn -cnamazonnow.com.cn -cn amazonsdi.comamazonstudiosguilds.comamazontrust.comamazonvideodirect.com amzn.com amzn.to  amznl.comassoc-amazon.cn -cnassociates-amazon.comgameon-masters.comgdansk-amazon.commedia-amazon.com primeday.cn -cnprimeday.com.cn -cn primeday.infosiege-amazon.comssl-images-amazon.comueberamazon.de -yamaxun.cn -cn yamaxun.comz.cn -cn audible.comacmvalidations.comacmvalidationsaws.comaesworkshops.comamazonaws-china.com amazonaws.com amazonaws.tvamazonworkdocs.cn -cnamazonworkdocs.com!amazonworkdocs.com.cn -cnamplifyapp.comamplifyframework.comaws-iot-hackathon.comawsautopilot.comawsautoscaling.com awsbraket.comawscommandlineinterface.comawsedstart.comawseducate.comawseducate.netawseducate.orgawsloft-johannesburg.comawsloft-stockholm.comawssecworkshops.com awsstatic.comawsthinkbox.com awstrust.comcdkworkshop.comcloudfront.netcontainersonaws.comelasticbeanstalk.comthinkboxsoftware.com".+\.awsdns-[0-9][0-9]\.co\.uk$.+\.awsdns-[0-9][0-9]\.com$.+\.awsdns-[0-9][0-9]\.net$.+\.awsdns-[0-9][0-9]\.org$".+\.awsdns-cn-[0-9][0-9]\.biz$!.+\.awsdns-cn-[0-9][0-9]\.cn$".+\.awsdns-cn-[0-9][0-9]\.com$".+\.awsdns-cn-[0-9][0-9]\.net$ kindle.cn -cn kindle.co.jp kindle.co.uk -kindle.com  kindle.de  kindle.es  kindle.fr  kindle.in  kindle.it  kindle.jpkindleoasis.cn -cnkindleoasis.comkindleoasis.com.cn -cnkindleoasis.infokindleoasis.jpkindleoasis.orgkindleoasis.uskindleoasisnews.comkindleproject.comamazonprimevideo.cn -cn#amazonprimevideo.com.cn -cnamazonprimevideos.comamazonvideo.ccamazonvideo.comprime-video.com primevideo.ccprimevideo.comprimevideo.infoprimevideo.org primevideo.tv+images-cn.ssl-images-amazon.com -cn-!images-cn-8.ssl-images-amazon.com -cn -) - LEANPLUM-ADS leanplum.com -ads -2 -REABBLE -reabble.cn -cn reabble.com - -3TYPE 3type.cn - -CNKI cnki.net - - INMEDIAHK inmediahk.net - - HELLOFONT hellofont.cnhellowebfont.com0,hellofontpreview.oss-cn-beijing.aliyuncs.com-)hellowebfonts.oss-cn-beijing.aliyuncs.com - -ITU itu.int - -TWISTERtwister.net.co - -TWITTERads-twitter.com periscope.tv pscp.tvt.co tellapart.com tweetdeck.com  twimg.com twitpic.com twitter.com -twitter.jptwitterinc.comtwitteroauth.comtwitterstat.us twtrdns.net vine.co - -VOANEWSamerikaninsesi.comamerikaninsesi.orgamerikaovozi.comamerikayidzayn.comamerikiskhma.com bbg.govdandalinvoa.com darivoa.comdengeamerika.comdengiamerika.comglasamerike.netgolos-ameriki.ru insidevoa.com pashtovoa.com persagg.comradiyoyacuvoa.com urduvoa.com voa.govvoaafaanoromoo.comvoaafrique.comvoabambara.com voabangla.comvoacambodia.comvoacantonese.comvoachinese.comvoadeewanews.comvoadeewaradio.com voahausa.comvoaindonesia.com voakorea.comvoalingala.comvoandebele.com voanews.com -voanews.euvoanoticias.com voanouvel.comvoaportugues.com voashona.com voasomali.comvoaswahili.com voathai.comvoatibetan.comvoatibetanenglish.comvoatiengviet.com voatour.comvoazimbabwe.comzeriamerikes.com -P -AEROGARDaerogard.com.auaerogard.com.cn -cnaerogardcn.com - -CATEGORY-COMPANIESacer-group.com acer.com acer.com.cn -cn typekit.com typekit.net10xfotolia.com 2o7.net acrobat.com adbecrsl.comadobe-aemassets-value.comadobe-audience-finder.com"adobe-video-partner-finder.com  adobe.com adobe.lyadobeaemcloud.comadobeaemcloud.netadobeawards.comadobecreativityawards.com adobedc.net adobedemo.com adobedtm.comadobeexchange.comadobeexperienceawards.com adobegov.comadobehiddentreasures.comadobejanus.com adobeku.comadobelogin.com adobeoobe.comadobeplatinumclub.comadobeprojectm.com adobesc.com adobesign.comadobesigncdn.comadobespark.com adobess.comadobestock.com adobetag.comadobetarget.comadobetcstrialdvd.comadobetechcomm.comadobetechcommcallback.comadobetechcommdemo.comadobexdplatform.comassetsadobe.com authorxml.combusinesscatalyst.com ccnsite.com  ccpsx.com -demdex.netdollarfotoclub.comdollarphotoclub.comdollarphotosclub.comdouwriteright.com echocdn.com echosign.com edgefonts.netenablementadobe.com  ftcdn.net gostorego.comimagineecommerce.commacromedia.commagentoliveconference.commarketing-nirvana.com omniture.com -omtrdc.net photoshop.complacesdocs.comsundanceignite2016.comworldsecureemail.comworldsecuresystems.com adobe.iodevelopria.com behance.nets2stagehance.com adobecc.com adobecce.comadobeccstatic.comcreativecloud.comcreativesdk.com cotolia.com ffotolia.com fiotolia.com foftolia.com fonolia.com fotiolia.com fotoiia.comfotolia-noticias.com -fotolia.cc fotolia.com -fotolia.tv fotolja.com fptolia.com gfotolia.com motolia.com photolia.nettenbyfotolia.com votolia.combluefootcms.com mageconf.commageconf.com.ua magento.com magento.netmagentocommerce.commagentomobile.commarketing-cloud.com marketo.co.uk marketo.com marketo.net -marketo.tvmarketodesigner.commarketolive.com -mktdns.commkto-c0100.com mktorest.com mktroute.commobilemarketo.com&"revenue-performance-management.com toutapp.comalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.com amazon-adsystem.com -adswholecitiesfoundation.orgwholefoodsmarket.comwholekidsfoundation.orgwholeplanetfoundation.orgaws -amazonimdb -kindle xn--cckwcxetdxn--jlq480n2rg yamaxun -zapposaboutamazon.cn -cnaboutamazon.co.ukaboutamazon.comaboutamazon.com.auaboutamazon.deaboutamazon.esaboutamazon.euaboutamazon.fraboutamazon.inaboutamazon.itaboutamazon.jpaboutamazon.pl alexafund.cn -cnalexafund.com.cn -cn amaaozn.comamazon-fashions.comamazon-jp-recruiting.comamazon-lantern.comamazon-launchpad.com  amazon.ae  amazon.ca amazon.cn -cn amazon.co.jp amazon.co.uk -amazon.com amazon.com.au amazon.com.br amazon.com.mx amazon.com.tr  amazon.de  amazon.es  amazon.fr  amazon.in  amazon.it amazon.jobs  amazon.jp  amazon.nl -amazon.red  amazon.sgamazonalexavoxcon.comamazonauthorinsights.comamazonbusiness.cn -cn!amazonbusiness.com.cn -cnamazonbusiness.orgamazonbusinessblog.comamazonchoice.cn -cnamazonchoice.com.cn -cnamazonchoices.cn -cn amazonchoices.com.cn -cnamazondevicesupport.comamazonfctours.comamazonianblog.comamazonimages.comamazoninspire.cn -cn amazoninspire.com.cn -cnamazonlaunchpad.cn -cnamazonlaunchpad.com"amazonlaunchpad.com.cn -cn amazonlending.com.cn -cn!amazonliterarypartnership.comamazonlumberyard.wang amazonnow.cn -cnamazonnow.com.cn -cn amazonsdi.comamazonstudiosguilds.comamazontrust.comamazonvideodirect.com amzn.com amzn.to  amznl.comassoc-amazon.cn -cnassociates-amazon.comgameon-masters.comgdansk-amazon.commedia-amazon.com primeday.cn -cnprimeday.com.cn -cn primeday.infosiege-amazon.comssl-images-amazon.comueberamazon.de -yamaxun.cn -cn yamaxun.comz.cn -cn audible.comacmvalidations.comacmvalidationsaws.comaesworkshops.comamazonaws-china.com amazonaws.com amazonaws.tvamazonworkdocs.cn -cnamazonworkdocs.com!amazonworkdocs.com.cn -cnamplifyapp.comamplifyframework.comaws-iot-hackathon.comawsautopilot.comawsautoscaling.com awsbraket.comawscommandlineinterface.comawsedstart.comawseducate.comawseducate.netawseducate.orgawsloft-johannesburg.comawsloft-stockholm.comawssecworkshops.com awsstatic.comawsthinkbox.com awstrust.comcdkworkshop.comcloudfront.netcontainersonaws.comelasticbeanstalk.comthinkboxsoftware.com".+\.awsdns-[0-9][0-9]\.co\.uk$.+\.awsdns-[0-9][0-9]\.com$.+\.awsdns-[0-9][0-9]\.net$.+\.awsdns-[0-9][0-9]\.org$".+\.awsdns-cn-[0-9][0-9]\.biz$!.+\.awsdns-cn-[0-9][0-9]\.cn$".+\.awsdns-cn-[0-9][0-9]\.com$".+\.awsdns-cn-[0-9][0-9]\.net$ kindle.cn -cn kindle.co.jp kindle.co.uk -kindle.com  kindle.de  kindle.es  kindle.fr  kindle.in  kindle.it  kindle.jpkindleoasis.cn -cnkindleoasis.comkindleoasis.com.cn -cnkindleoasis.infokindleoasis.jpkindleoasis.orgkindleoasis.uskindleoasisnews.comkindleproject.comamazonprimevideo.cn -cn#amazonprimevideo.com.cn -cnamazonprimevideos.comamazonvideo.ccamazonvideo.comprime-video.com primevideo.ccprimevideo.comprimevideo.infoprimevideo.org primevideo.tv+images-cn.ssl-images-amazon.com -cn-!images-cn-8.ssl-images-amazon.com -cn amd.comamdfanstore.com -radeon.comiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -adscalendarserver.orgapple-cloudkit.com cups.orgdeveloper.apple.comdevimages-cdn.apple.comdevstreaming-cdn.apple.comfoundationdb.orgmacosforge.org macruby.com macruby.net macruby.orgapple-mapkit.complaygrounds-cdn.apple.com carekit.orgresearchandcare.orgresearchkit.cn -cnresearchkit.com.cn -cnresearchkit.hkresearchkit.netresearchkit.orgresearchkit.tvservicetalk.io -webkit.org webkitgtk.org wpewebkit.orgwebobjects.com-)appleworldwidedeveloper.hb-api.omtrdc.net)%appleworldwidedeveloper.sc.omtrdc.net#p-events-delivery.akamaized.net100beatscheap.com1monsterbeatsbydreus.com2013beatsbydrdreonline.com2013beatsbydrdreshop.com2013beatsbydreshop.com2013beatshdcybermonday.com2013cheapestbeats.com2013newbeatsworld.com2013pascherbeatsbydre.com2014cheapbeatsbydre.com 2beatsdre.com2buybeatsbydre.com2drdrebeats.com40shopping.com4beatsbydre.com5beatsbydre.comaaagradeheadphones.com -aanaan.comabcheadphones.comabeatsbydrdre.com aeasyshop.com -airmay.com -ajtalk.com albeats.com annstores.netaokwholesale.netaucheapbeats.comaudiobeatsau.comaudiobeatsbydre.comaudiomonsterbeatsonline.comauricularemonsterbeats.com#auricularesbeatsbaratosshop.comauricularesbeatsmarkt.comausbeatsbydrdre.combag-glasses1.com bandgirlz.com baxsound.com bbestmall.com -beatbd.combeatbydre2013.combeatbydremonster.com!beatbydreheadphonesonsale.combeatbydrekopen.combeatbydreuk2014.combeatdrdres.combeatfactoryoutlets.combeatmonstersaustralia.netbeats-by-dre-australia.combeats-bydrdre.netbeats-bydre-mall.combeats-bydrecheapsale.combeats-bydreoutletonline.combeats-bydreoutletsale.combeats-bydreoutletsale.netbeats-bydreoutletssale.netbeats-bydrestore.combeats-bydreuk.combeats-deal.combeats-dre-us.com"beats-headphones-buy-cheap.combeats-headphones.usbeats-sale.combeats-seller.combeats-soaho.com  beats1.cc beats1.cn -cn  beats1.tv beats1.com.cn -cn beats123.com beats2.com.cn -cn beats4.cn -cn -beats4.netbeats4outlets.combeats4salecheap.combeatsallsale.combeatsaudifonos.combeatsaudiobydre.combeatsaudiobydre.netbeatsaudios.netbeatsbeatsmonster.combeatsbestdeals.combeatsblackfridaydeals.netbeatsblackfridayretails.combeatsboxingdayuksale.combeatsbydrdre-headphones.combeatsbydrdre-officials5.combeatsbydrdre-online.combeatsbydrdre-onsale.combeatsbydrdre-store.combeatsbydrdre-store.usbeatsbydrdre4sale.combeatsbydrdrebiz.combeatsbydrdrecustom.combeatsbydrdredanmark.combeatsbydrdremall.combeatsbydrdreoutlet.combeatsbydrdres.combeatsbydrdresale.netbeatsbydrdrestore.combeatsbydrdreus.com!beatsbydre-beatsheadphone.combeatsbydre-chen.combeatsbydre-club.combeatsbydre-headphones.com!beatsbydre-headphonesshop.combeatsbydre-mall.combeatsbydre-outlet.combeatsbydre-outletsale.netbeatsbydre-outletstore.combeatsbydre-sell.combeatsbydre-store.combeatsbydre-studio.combeatsbydre-us.com beatsbydre.jpbeatsbydre2081.combeatsbydre411.combeatsbydre4usales.combeatsbydreauofficial.combeatsbydreausale.net beatsbydreaustralia-sale.com!beatsbydreaustraliaonline.com"beatsbydreaustraliaonlines.com beatsbydreaustraliasales.combeatsbydrebeatsby.com!beatsbydreblackfriday2013.com beatsbydreblackfridaypro.combeatsbydreboxingdayca.combeatsbydrecasquesfr.com#beatsbydrecheap-outletstore.combeatsbydrecheaper.com"beatsbydrecheaponlinesales.combeatsbydrecolors.com beatsbydrecustomwireless.com&"beatsbydrecybermondaydeals2013.com&"beatsbydrecybermondaydeals2013.netbeatsbydredanmarks.com"beatsbydredealsblackfriday.com"beatsbydredealscybermonday.combeatsbydrediscount.com beatsbydrediscountonline.netbeatsbydredr.combeatsbydreexecutivesale.com beatsbydreformall2013-nl.com beatsbydreforsalesonline.com beatsbydreforshop2013-nl.combeatsbydreforstore.combeatsbydreforyououtlet.combeatsbydrefr.com!beatsbydrefrcasquepascher.combeatsbydrefriday.combeatsbydregot.combeatsbydrehd.combeatsbydrehd.netbeatsbydreheadphones-nz.combeatsbydreheadphonesolo.combeatsbydrehut.combeatsbydreinexpensive.combeatsbydreireland-sales.com beatsbydreirelandonlines.combeatsbydreirelandsale.com beatsbydreirelandsonline.combeatsbydremall2013.com"beatsbydremonsteraustralia.combeatsbydrenls.combeatsbydrenorge1.net!beatsbydreofficialdanmark.combeatsbydreoksale.combeatsbydreol.combeatsbydreonlie2013-nl.com!beatsbydreonlines-ireland.combeatsbydreonlines-uk.combeatsbydreonlinesale-nz.combeatsbydreoordopjes.combeatsbydreoslo.combeatsbydreoutletsale.combeatsbydreoutletscheap.combeatsbydrerealstore.combeatsbydres-shop.combeatsbydresale-uk.combeatsbydresalemall2013.combeatsbydresales.us'#beatsbydresalesonline-australia.com beatsbydresaleonlines-nz.combeatsbydresdanmark.netbeatsbydreshop-uk.combeatsbydreshops.netbeatsbydresingapores.combeatsbydresingaporesale.combeatsbydrespeakers.combeatsbydresold.com%!beatsbydresolohdonline-canada.combeatsbydresonline-nz.combeatsbydrestorevip.combeatsbydressale.combeatsbydressolo.combeatsbydresstudio.com"beatsbydrestudio-australia.combeatsbydrestudio.combeatsbydretoutlet.combeatsbydreuk.combeatsbydreuk.netbeatsbydreus.combeatsbydrevipde.combeatsbydrew.combeatsbydrsmonsterinusa.combeatsbymusic.netbeatsbysdrbre.combeatsbysdrdres.combeatscasque-france.combeatscheap-nz.combeatscheap-uk.combeatscheapforsale.combeatscollection2014.combeatscustomblackfriday.combeatsdanmark2013.combeatsdrdre-headphones.combeatsdrdre-it.combeatsdrdre-solo.combeatsdrdre2014.combeatsdrdrecuffie.netbeatsdrdrekaufenschweiz.netbeatsdrdreneon.combeatsdre-monster.com beatsdre.netbeatsdre4cheap.combeatsdrecheap.combeatsdredreheadphones.combeatsdreforsale.combeatsdreinau.combeatsdremonster-uk.com!beatsdrenewcolorful4usale.combeatsdreoutletsale.combeatsdresale2013.combeatsdresalestore.combeatsdresolo2013.combeatsdreus.combeatsearbudsheadphoness.combeatsfacstore.combeatsfactoroutlets.combeatsfactorycollection.combeatsfactoryoutles.combeatsforcheap-usa.combeatsforme.combeatsfranceofficiel.combeatselectronic.netbeatselectronics.com -beatsep.cn -cn beatsep.com beatsep.net beatsheadphones-discount.combeatsheadphones1.combeatsheadphones2u.combeatsheadphonesale.combeatsheadphonesdealer.combeatsheadphonesforcheap.netbeatsheadphonesonline.combeatsheadphoness.combeatsheadphonestudio.combeatsheadphonesusamall.combestshoesale2014.netbeatshopuk.combeatsincanada.combeatsinsingapore.combeatsireland.net beatsjoy.combeatsmonstersales.combeatsmusic.combeatsmusic.wang beatsneon.combeatsnzsale.com beatsodre.combeatsofdre-usa.combeatsonblackfriday2013.com beatsone.netbeatsoutlet.netbeatsoutletanytime.combeatsoutletonlines.combeatspascher-bydre.combeatspascher-fr.netbeatspascher.netbeatspaschers.netbeatspillnewcolor.combeatspills.combeatspromonsterjp.combeatspromotions.combeatssales.combeatssaleus.combeatssaustraliabuy.combeatssbyaustralia.combeatssbydredanmark.combeatsshop-uk.combeatsshop-usa.combeatsshopstore.combeatssingapores.combeatssingaporeshop.com beatsstudiohodetelefoner.combeatsstudiosite.combeatstoreusa.com beatsua.combeatswholesale.usbeatswirelesscheap.combeatswirelesscuffie.combegin-trade.combestbeats4u.com -biitii.comblackfridaydrebeatsnew.comblackfridaydrebeatsshop.combloommicroventures.com bookonsky.net -bossae.combrandeasygo.combrandelectronic.combrandproducts1688.com brands098.combusinesswebwise.combuy-from-shanghai.combuybeatsbydre-uk.combuybeatsheadphonesbydre.combuycheapbeatsbus.combuycheapbeatsbydre.combuycheapbeatsbydreshop.combuycheapbeatsdreuk.combuydrdrebeatbox.combuyminibeatbox.combuyonlineheadphones.combuyshoponly.combuzzardflapper.combybeatsdre.combydrebeats.comcanada-beatsbydre.com casque-fr.com casquebeatsbydrdresolohd.comcasquebeatsdocteurdre.comcasquebeatsdre2013.comcasquebeatsenligne.comcasquebeatsfracheter.comcasquebeatsmer.netcasquebeatsofficiel-fr.comcasquebeatspascher2013.comcasquebeatssolo.netcasquedrdrebeatssfr.comcasquemonsterbeats.com#casquemonsterbeatsbydre2013.comcasquesbeatsaudio.comcheap-beats-by-dre.netcheap-beatsbydre.comcheapbagshoes.com cheapbeats.uscheapbeats365.comcheapbeats4sale.netcheapbeatsaustraliasale.comcheapbeatsbus.comcheapbeatsbydr.comcheapbeatsbydrdrepro.comcheapbeatsbydre-au.comcheapbeatsbydre99.comcheapbeatsbydrefau.comcheapbeatsbydremall.comcheapbeatsbydremonster.comcheapbeatsbydrenz.netcheapbeatsbydreonsale.com cheapbeatsbydreoutlet-nz.comcheapbeatsbydreoutlet.com"cheapbeatsbydreoutlets2013.comcheapbeatsbydresale.comcheapbeatsbydreshop.comcheapbeatsdrdresolo.com$ cheapbeatsbydrestudioedition.com"cheapbeatsbydrestudioutlet.comcheapbeatsdrestudios.comcheapbeatsheadphone2014.comcheapbeatsheadphones.uscheapbeatsie.comcheapbeatsla.comcheapbeatssale4u.comcheapbeatsshopbydre.comcheapcustombeatsbydre.comcheapdrdrebeats8.comcheapdrdrebeatsca.comcheapdrebeats8.netcheaperbeatsbydresale.comcheapestbeatsdrdre.comcheapheadphonesland.comcheapheadphonessale.comcheapheadsetmall.comcheapheadsetshop.com cheapmonsterbeatsbydrdre.com!cheapmonsterbeatsheadsets.com cheapmonstersbeatsonsale.comcheapmonsterbeatssale.comcheapmonsterbeatsusa.uscheapnewbeatsbydre.comcheapsalemonster.comcheapshoesvip.comcheapwireless04.comcheapwirelessbeats.comcheerwholesale.uschihair-straightener.comchinaclothesstore.comciattackers.com -cmhalq.com cnshopin.comcoolmonster.netcozydrdrebeats.com cqcorea.comcuffiesaldi.comcustombeatsbydrebuy.comcustombeatsdeals.comcustombeatsforcheap.comcustombeatsny.comcustombeatssbydreus.comcustomdrdrebeats.comcustomizedbeatbydre.comcustomizedbeatsbydre.comcustomizedbeatsdre.comcybermondaybeats4sale.comdanemarket.comdanmarkbeatsbydrdre.comdctbeatsbydre.comdealsbeatsblackfriday.comdetaliczny.comdiscountbeatsbydre-us.comdiscountbeatsstore.comdkbeatsbydre.comdrbeatsukmart.comdrdre-beats.comdrdrebeats-chen.comdrdrebeats-headphone.comdrdrebeats-usa.comdrdrebeatsale.comdrdrebeatsbillig.comdrdrebeatsdesale.comdrdrebeatsdiscount.comdrdrebeatsforu.comdrdrebeatsretail2013.comdrdrebeatssale7.comdrdrebeatsuk.comdrdreheadphonebeats.comdrdreheadphonekey.comdrdreheadphonesusstore.comdrdremonster-beats.comdrdremonsterdre.comdrdreprobeatssale.com drdrefnac.comdrebeats-australia.comdrebeats-france.comdrebeats-monster.com!drebeats-monsteraustralia.comdrebeats-monsterusa.comdrebeats-singapore.comdrebeats-singapore.netdrebeats-singaporecheap.comdrebeats-singaporecheap.netdrebeats-solo.comdrebeats-studio.comdrebeatsaustralia-cheap.comdrebeatsaustralia-cheap.netdrebeatsbuy.comdrebeatsbydreoutlet.comdrebeatscanada.comdrebeatsdeutschland.netdrebeatsforsaleus.comdrebeatsheadphones-nz.comdrebeatshome.comdrebeatsoutletstore.comdrebeatsoldes.comdrebeatspill.comdrebeatssite.com#drebeatssolocybermondaysale.comdrebeatsstudio2013.comdrebeatstudio.com drebyby.comdremonsterbeatsoutlets.comdreprobeats.com dreprofy.com -dtsell.comearphonescheapest.comebuyheadphones.comesbeatsbydrebuy.cometheadphones.comfamosascalvas.comfindmybeats.comfr-beatsbydrestore.comfrancecasquebeatssolde.comfrcasquesbeats.com free-aa.com free-sns.com frishoes.comgamebeforethegame.comgetdrebeatssale.comgmnetworks.net gobeatsye.comgobuyonlinestore.net goodsdunk.netheadphonepubs.comheadphones-cool.comheadphones-dre.com headphones-outlet-online.comheadphonesbeatsaudio.comheadphonesbeatsbydre.comheadphoneses.comheadphoneshome.comheadphoneshotsales.comheadphonesol.comheadphonesretailer.comheadphonessupply.comheadphonezip.comheadset987.comhellokittybeats.com highbolt.net highdefinitionbeatsbydre.com hipaa6020.com hipzoom.nethonawalaan.comhotbeatsonsale.comhotmonsterbeats.comhottestheadphonesonline.com  hrsaz.com ibeats-uk.comibeatsbydre.ccibeatsbydre.comigoshopping.net illianacomputerrecycling.comimonsterbeats.comincentivetravelgifts.comioffergift.com islamiccenterofnewlondon.comitaliabeatsbydrdre.com jkbeats.comkickshatchannel.comkobbeatssbydredk.comkopfhorergunstigshop.comleecountytimes.com lexuemei.comlovebeatsdr.com lovemarca.comluckyissue.com -lyzsxx.com mairbeats.com -makeeu.commallheadphone.commbeats-tech.commbheadphone.com -micstl.commmonsterheadphones.netmonbeats2013.commonster-beats-by-dr-dre.com monster-beats-headphones.commonsterbeats-cheap.commonsterbeats-headphone.commonsterbeats-onsale.commonsterbeats-solo.commonsterbeats-solo.netmonsterbeats365buy.commonsterbeats8beatsbydre.commonsterbeatsale.commonsterbeatsalestore.commonsterbeatssalg.commonsterbeatsau.commonsterbeatsbydrdre-nz.commonsterbeatsbydrdre-usa.com!monsterbeatsbydrdrecanada.com!monsterbeatsbydrdrestudio.commonsterbeatsbydre-nz.commonsterbeatsbydre-usa.commonsterbeatsbydre2015.com'#monsterbeatsbydreaustraliacheap.com!monsterbeatsbydrebilligde.com monsterbeatsbydrefactory.commonsterbeatsbydrenew.commonsterbeatsbydres.commonsterbeatsbydreshop.commonsterbeatscasques.commonsterbeatscommunity.commonsterbeatsdrdrecheap.commonsterbeatsfactory.netmonsterbeatsfinland.commonsterbeatsforsale.commonsterbeatsheadphone.commonsterbeatsheadphones.netmonsterbeatshere.commonsterbeatsitaly.commonsterbeatsmall.commonsterbeatsnegozi.netmonsterbeatsok.com!monsterbeatsonlinestoreuk.commonsterbeatsoutlet.usmonsterbeatspascher.netmonsterbeatsru.commonsterbeatssales.commonsterbeatsshops.netmonsterbeatsstore.com#monsterbeatssydneyaustralia.commonsterbeatstang.commonsterbeatstienda.commonsterbeatstudio.commonsterbydrebeat.com!monstercasquebeatspascher.netmonstercheapbeatss.commonsterdrebeats-canada.netmonsterdrebeats-usa.commonsterdrebeats-usa.netmonsterheadphone.netmonsterproduct.netmonstersbeatbydres.commonstersdebea.commonstershopcheapbeats.netmoreheadphones.com -ms4dre.commybeatsbydrestudio.commybeatsbydreuk.commybeatscheapbydre.commyferrariheadphones.comnamemybeats.com neborder.comnegozimonsterbeats.comnegoziomonsterbeats.comnewbeatsblackfriday.com newbemany.comnewdrediscount.comnewestbeatsbydre.comnewlysprung.netnewmonst1erbeatsto1re.com!newmonsterbeatsheadphones.comnewschristmasshopping.comnflhotsalejerseys.com nicesshop.netofferairjordanlebron.comofficialbeatsbydreshop.comofficialbeatsbydrestore.comofficialdrdre.comofficialheadphone.com ogameblog.comonbeatsbydresale.com onlinemonsterbeatsonsale.com onsalekey.comonsaletrend.comoutletbeatsshop.compascherbeatsmonster.compaulsimon-music.comperfectkickz.netpersoneelsland.compicknicekicks.netpickshoesclothes.com pillbeatsblackfridaysale.compocketbiketrader.compowerbeats2wireless.compowerbeatsbydrdre.compowerbeatsbydre.compromonsterbeatsbydre.comprostudiobeatscybersale.com  rbsgr.comrepsneakermall.com repswing.com!salebeatslasteststyle4you.comsaleblackfridaydrebeats.comsalecheaphandbags.com samebags.com sanvaras.com -scnshop.cc -shoers.com shoestop2.comshop-headphones.netshopbeatsdre.com shopbydre.comshopcustomizedbeats.comshopdrebeats.comshopmonsterbeats.comshopping-days.netshoppinguheadphones.comsneaker666.comsneakerpage.netsneakerskick.comsolostudioksale.com soso7778.comspecialtyheadphones.comspiritclubs.comstraightouttasomewhere.comstudiobeatsbydrdre.comsulwerphoto.comsunglassessale2014.comsuperearsenjoy.comsupplybestjerseys.comsustainthesound.comsverigebeatsbydrdre.com szcheapmonsterheadphones.comszwinnertechnology.com takegoto.comthe-monster-beats.comthebeatsbydre.comthebeatsbydre.netthebeatsheadphonesale.com -theuab.nettopbeatsbydrdreoutlet.comtopbeatsdealer.comtopbeatsforsale.com tradevip1.com tteshop.com ubnw.net -unstyle.us&"usa-beatsbydreheadphonesonsale.net  v-has.com vip-beats.comvipbeatsbydre.comvipheadphones.com vipshoes2.com  wasdj.comwholesale-exporter1.comwholesalediscountpurses.comwholesalefine.comwholesaleonlinemart.comworldcoinpay.comyourmonsterbeats.com%!beatselectronicsllc.tt.omtrdc.netapple-icloud.cn -cnappleicloud.cn -cnapplemx-icloud.comicloud-apple.cn -cnicloud-content.comicloud-isupport.com  icloud.ch -icloud.com icloud.com.cn -cn  icloud.de  icloud.ee  icloud.fi  icloud.fr  icloud.hu  icloud.ie  icloud.is  icloud.jp  icloud.lv icloud.net.cn -cn  icloud.om -icloud.org  icloud.pt  icloud.ro  icloud.se  icloud.si  icloud.sk  icloud.vn icloudads.neticloudapple.cn -cn icloudbox.neticloudbrowser.net icloude.comicloudhome.comicloudmail.neticloudmusic.net icloudnet.net icloudpay.net icloudo.com -icloudo.de icloudo.net icloudos.de icloudos.neticloudsecure.neticloudsetup.comios-icloud.com -me.com myicloud.net mylcloud.net wwwicloud.com!www-cdn.icloud.com.akadns.net!12diasdepresentesdeitunes.com12diasderegalosdeitunes.cl12diasderegalosdeitunes.co!12diasderegalosdeitunes.co.cr!12diasderegalosdeitunes.co.ni!12diasderegalosdeitunes.co.ve12diasderegalosdeitunes.com"12diasderegalosdeitunes.com.co"12diasderegalosdeitunes.com.hn"12diasderegalosdeitunes.com.ni"12diasderegalosdeitunes.com.ve12diasderegalosdeitunes.cr12diasderegalosdeitunes.gt12diasderegalosdeitunes.hn12diasderegalosdeitunes.pe 12joursdecadeauxdeitunes.comapple-itunes.cn -cn itun.es itunbes.com ituneas.comitunes-apple.cn -cnitunes-radio.net  itunes.ca  itunes.co itunes.co.th -itunes.com  itunes.hk  itunes.mx -itunes.org -itunes.rio  itunes.usitunes12days.comitunesapple.cn -cnitunesessentials.comitunesfestivals.comitunesiradio.comituneslatino.comituneslogin.netitunesmatch.comitunesparty.comitunesradio.cn -cnitunesradio.comitunesradio.com.cn -cnitunesradio.rioitunesradio.tvitunesradio.twitunesstore.co itunesu.com itunesu.net iutunes.com wwwitunes.comitunes-apple.com.akadns.netappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cn apple apple.ae apple.at apple.be apple.bg apple.bs apple.ca apple.ch apple.cl apple.cmapple.cn -cn apple.co apple.co.cr apple.co.hu apple.co.jp apple.co.kr apple.co.mz apple.co.nz apple.co.th apple.co.uk  apple.com apple.com.af apple.com.au apple.com.bo apple.com.br apple.com.cn -cn apple.com.co apple.com.de apple.com.do apple.com.gr apple.com.gy apple.com.jm apple.com.lk apple.com.mg apple.com.mx apple.com.my apple.com.pa apple.com.pe apple.com.pl apple.com.pr apple.com.pt apple.com.py apple.com.sg apple.com.sv apple.com.tr apple.com.tw apple.com.uy apple.cz apple.de apple.dk apple.ee apple.es apple.eu apple.fi apple.fr apple.hamburg apple.hn apple.hr apple.hu apple.ie apple.in apple.is apple.it apple.jo apple.jp apple.kr apple.lk apple.lt apple.lv apple.me apple.my  apple.net apple.net.gr apple.nl apple.no apple.pk apple.pl apple.pt apple.ro apple.rs apple.ru apple.sa apple.se apple.sg apple.si apple.sk apple.so apple.tt apple.tw apple.uk apple.usapple.xn--czr694bapple.xn--fiqs8s  apple.xyz aplestore.comapple-store.cn -cnapple-store.netapple-store.wang applestor.com applestore.bg applestore.cc applestore.ch applestore.cm applestore.cn -cnapplestore.co.huapplestore.co.jpapplestore.co.ugapplestore.co.ukapplestore.comapplestore.com.auapplestore.com.bnapplestore.com.cn -cnapplestore.com.eeapplestore.com.egapplestore.com.grapplestore.com.hkapplestore.com.hrapplestore.com.joapplestore.com.myapplestore.com.phapplestore.com.plapplestore.com.ptapplestore.com.roapplestore.com.ruapplestore.com.snapplestore.com.tw applestore.de applestore.hk applestore.kr applestore.meapplestore.netapplestore.net.gr applestore.ph applestore.qa applestore.sgapplestore.wangapplestoreonline.comapplestorepro.euonlineapplestore.comapp-store.wangappe-store.comapple-appstore.cn -cnappleappstore.cn -cnappleappstore.netappleappstore.tv  appsto.reappstore.co.id appstore.hk appstore.my appstore.phappstoreapple.cn -cn asto.retvappstore.netapple-imac.com appleimac.comimac-apple.comimac-applecomputer.com imac.co -imac.co.nz imac.com imac.eu imac.gr imac.one imac.rs imacapple.comimacapplecomputer.comebookforipad.com euroipad.comfindmyipad.com indiaipad.com -ipad.co.kr ipad.de  ipad.host  ipad.wang  ipad3.com -ipadair.cl -ipadair.cmipadair.com.bripadair.com.es -ipadair.fr -ipadair.hk -ipadair.ie -ipadair.jp -ipadair.twipadaustralia.com ipadmini.cmipadmini.com.lk ipadmini.lk ipadpro.buzz -ukipad.comappleiphone.huappleiphone.netappleiphonecell.combestiphonestuff.comcooliphonecasesstore.comcustom-iphonecase.comdesigneriphonescases.com dl-iphone.comfundaiphone5s.com hebiphone.com hf-iphone.com -iphine.comiphone-8.com.cn -cn iphone-cd.com iphone-cn.com iphone-sh.comiphone-vip1.comiphone-vip2.comiphone-vip3.comiphone-vip4.comiphone-vip5.com iphone-x.tv iphone-yh.com iphone-zh.com  iphone.cm -iphone.com iphone.com.au iphone.com.gr  iphone.es iphone.host iphone.net.gr  iphone.pt  iphone.rs  iphone.ru iphone.wang iphone4.comiphone4.com.br iphone4s.com iphone5.comiphone5casescovers.comiphone5s5case.comiphoneacessorios.com.briphonecase2013.comiphonecase5.comiphonecases100.comiphonecases5.comiphoneclaro.com.briphonecollcase.com iphonefc.comiphonegermany.comiphonehangzhou.comiphoneimessage.comiphoneplus.wang iphonerip.net iphonese.tviphoneunlockimei.com iphonexs.tvonlyiphone5case.comaplleipods.comappleclassicipod.comdownloadsforipod.com ipod.ca ipod.ch ipod.cm ipod.co -ipod.co.nz -ipod.co.uk -ipod.co.za ipod.com ipod.com.au ipod.com.cn -cn ipod.com.fr ipod.com.hk ipod.com.sg ipod.com.tw ipod.de ipod.es ipod.eu ipod.fr ipod.gr ipod.hk ipod.is ipod.net ipod.no ipod.pk ipod.rs ipod.ru ipod.tw ipodcentre.nlipodcleaner.comipoditouch.com ipodnano.com ipodnano.netipodprices.com -ipodrip.caipodrocks.com.au  ipods.comipodshop.com.au ipodtouch.co ipodtouch.com -myipod.netoffrezdesipods.comsimplyipod.comwwwipodlounge.comimacsources.comlaunchpad.wang mac.wang -macbook.co -macbook.hk -macbook.tw macbook.wang macbookair.cn -cnmacbookair.co.krmacbookair.co.ukmacbookair.commacbookair.com.aumacbookair.com.cn -cnmacbookair.com.es macbookair.es macbookair.hk macbookair.kr macbookair.jpmacbookair.net macbookpro.comacbookpro.commacbookpro.com.aumacbookpro.net macbookpro.usmacbookpros.commacbooksale.comwwwmacbookair.comapple-watch.com.ru applewatch.hk applewatch.tv applewatch.twapplewatch.wangapplewatchedition.comapplewatchseries3.netapplewatchsport.comapplemusic.berlinapplemusic.comapplemusic.com.auapplemusic.hamburgapplemusic.wangapplemusicconnect.comapplemusicfestival.com musickit.netwwwapplemusic.comapple.comscoreresearch.com -apple.newsappleenews.comapplenews.berlinapplenews.hamburg applenews.tvapplenewsformat.com apple-pay.com apple-pay.rsapple-pay.wangapplepay.berlinapplepay.co.rsapplepay.com.twapplepay.hamburg applepay.hk applepay.info applepay.jp applepay.rs applepay.tvapplepaycash.cn -cnapplepaycash.com.cn -cnapplepaycash.netapplepaycash.tv!applepaymerchantsupplies.infoapplepaysupplies.berlinapplepaysupplies.cn -cnapplepaysupplies.com#applepaysupplies.com.cn -cnapplepaysupplies.infoapplepaysupplies.netapplepaysupplies.tvapplewallet.comapplewallet.tv -i-book.com -i-book.net ibook.co.nz  ibook.com ibook.eu  ibook.netibookpartner.comibooksauthor.com  iphoto.eu  iphoto.no  iphoto.se iphoto.wang  imovie.euimoviegallery.comimoviestage.comapplecare.berlin applecare.cc applecare.euapplecare.hamburgapplecare.wangappledarwin.comappledarwin.netdarwinsource.comdarwinsource.orgdarwinsourcecode.comappleaccount.netappleid-applemx.comappleid-applemx.usappleid-iclou.com appleid-uk.usappleid.berlin appleid.comappleid.hamburg -appleid.hk ids-apple.com myappleid.com appletv.com -appletv.fr appletv.wang appletv4.cn -cnappletv4.com.cn -cnappletvapp.appleappleone.audio appleone.blog appleone.chatappleone.cloud appleone.clubappleone.community appleone.filmappleone.guide appleone.hostappleone.space appleone.techappleone.website -shazam.com1to1computing.com.au1to1conference.com.au 5lml.com -a0pple.net aapl.twairport.brussels airport.com -airport.euairsupportapp.com airtunes.com airtunes.info airtunes.netalchemysynth.com  ap0le.com aple.com  apole.com -app0le.com -appl-e.com appl.com -appl4e.com  appla.comapple-darwin.comapple-darwin.netapple-darwin.org apple-dns.cn -cn apple-dns.comapple-dns.com.cn -cn apple-dns.netapple-enews.comapple-expo.com apple-expo.eu apple-hk.comapple-ibooks.cn -cn apple-inc.netapple-livephotoskit.com apple-maps.cn -cnapple-online.com apple-usa.netappleafrica.comappleantilles.comappleaustralia.comappleaustralia.com.auappleaustralia.net.au applebk.netapplecarbon.com applecard.tvapplecentar.co.rsapplecentar.rsapplecenter.cn -cnapplecenter.com.cn -cnapplecentre.com.auapplecentre.infoappleclub.com.hk applecom.comapplecomputer-imac.comapplecomputer.cn -cnapplecomputer.co.inapplecomputer.co.nzapplecomputer.com applecomputer.com.cn -cnapplecomputer.com.hkapplecomputer.com.twapplecomputer.huapplecomputer.krapplecomputerimac.comapplecomputerinc.infoapplecomputers.co.nzapplecoronavirus.com appledns.cn -cnappledns.com.cn -cn appleexpo.euappleexpo.infoapplefilmaker.comapplefilmmaker.comapplefinalcutproworld.comapplefinalcutproworld.netapplefinalcutproworld.orgapplehongkong.comapplehongkong.com.hkappleinclegal.comappleipodsettlement.comappleiservices.com applelink.comapplemagickeyboard.comapplemalaysia.com.myapplemasters.infoappleonline.comappleonline.net appleoriginalproductions.com -appleos.tvapplepencil.netapplepodcasts.comapplepremiumreseller.com.au applepremiumresellers.com.auapplereach.comapplereach.net apples-msk.ruapplescreensavers.comapplescript.infoappleshare.infoappleshop.co.uk applesiri.cn -cnapplesurveys.comappletaiwan.com appletips.net appleweb.net applexpo.net applezh.com  applf.com -applfe.com -applle.com  applw.comappmediagroup.com -appule.com  appye.com  apyle.com  beddit.tvbraeburncapital.com burstly.netburstlyrewards.com buyaapl.com buyaapl.net buyaple.com camelphat.comcamelspaceeffect.com -carbon.com catalina.hk cdn-apple.com  chomp.com -coreml.netdashwood360.comdesktopmovie.comdesktopmovie.netdesktopmovie.orgdesktopmovies.netdesktopmovies.orgdeveloper-advisor.comdevice-manager.usdigitalhub.comdiscussionsapple.com -dot-mac.de  dotmac.dedvdstudiopro.bizdvdstudiopro.comdvdstudiopro.infodvdstudiopro.netdvdstudiopro.orgdvdstudiopro.us -earpod.net ecgapp.com.cn -cn -ecgapp.netedu-research.org -emac.co.in emac.in  emagic.de -eworld.com faceid99.com faceid99.netfaceidglobal.com faceshift.cn -cn facetime.netfinalcutpro.comfind-apple.com firewire.cl firewire.eu geoport.com -homepod.cn -cn -hopstop.tv ichat.co.in idvd.eu  ikids.comilecture.co.nz ilife.eu ilife.gr -ilife.wang imessage.tv insidear.cn -cninsidemacintosh.comiosinthecar.comipa-iphone.net  ipple.com ischool.com itools.info  iwork.com iwork.se -iwork.wangjetfuelapp.comjetfuelapps.comlatticedata.com livephotos.cn -cnlivephotos.com.cn -cn livephotos.tvlojaiphone.com.br mac-mini.com mac.com -mac.com.au -mac.eu -mac.rs macappsto.re macboxset.com mach-os.com mach-os.net -machos.net macintosh.eumacintoshsoftware.com macmini.com macos.com.aumacossierra.com macosx.infomacosxlion.com macpazar.com macreach.com macreach.net mobileme.dkmr-apple.com.tw mzstatic.com -newton.com next.com overapple.com powerbook.eupullstring.netquatrowireless.com quicktime.cc quicktime.comquicktime.com.au quicktime.eu quicktime.net quicktime.tvreform-apple.comservices-apple.net siri.com spektral.cc texture.comthinkdifferent.us -touchid.tv touchid.wangtracking-location.comtruedepth3d.com tryrating.comwebobjects.co.uk webobjects.de webobjects.euwebobjects.net wuxiapple.com wwapple.netwww-sms-apple.com wwwapple.com wwwapple.net wwwlapple.comxn--czrs0t4phtr3a.cnxn--fiqs8sxootzz.cn xn--fiqs8sxootzz.xn--hxt814e!xn--gtvq61aiijy0b.xn--hxt814exn--gtvz22d.wangxn--gtvz22d.xn--hxt814exn--hxtr4rozx.xn--czr694b xn--kput3imi374g.xn--hxt814e#xn--ohq11k7pl25iyo8a.cn -cn xn--ruq8a011kt6y.xn--hxt814e, adcdownload.apple.com.akadns.net -cn!adcdownload.apple.com -cn."amp-api-search-edge.apps.apple.com -cn"amp-api.apps.apple.com -cn aod.itunes.apple.com -cn#api-edge.apps.apple.com -cnappldnld.apple.com -cn"appldnld.g.aaplimg.com -cnapps.apple.com -cnapps.mzstatic.com -cn(apptrailers.itunes.apple.com -cn bag.itunes.apple.com -cn'bookkeeper.itunes.apple.com -cnbooks.apple.com -cn!cds-cdn.v.aaplimg.com -cn$cds.apple.com.akadns.net -cn cds.apple.com -cncl2-cn.apple.com -cn<0cl2.apple.com.edgekey.net.globalredir.akadns.net -cncl4-cn.apple.com -cn'client-api.itunes.apple.com -cn+clientflow.apple.com.akadns.net -cn clientflow.apple.com -cn cma.itunes.apple.com -cn."configuration.apple.com.akadns.net -cn#configuration.apple.com -cn!communities.apple.com -cncstat.apple.com -cn."dd-cdn.origin-apple.com.akadns.net -cn(discussionschinese.apple.com -cn(download.developer.apple.com -cnfides-pol.apple.com -cngs-loc-cn.apple.com -cngs-loc.apple.com -cn%gsp10-ssl-cn.ls.apple.com -cn!gsp11-cn.ls.apple.com -cn!gsp12-cn.ls.apple.com -cn!gsp13-cn.ls.apple.com -cnC7gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net -cn, gsp4-cn.ls.apple.com.edgekey.net -cn gsp4-cn.ls.apple.com -cn gsp5-cn.ls.apple.com -cn(gspe11-2-cn-ssl.ls.apple.com -cn&gspe12-cn-ssl.ls.apple.com -cn&gspe19-cn-ssl.ls.apple.com -cn-!gspe19-cn.ls-apple.com.akadns.net -cn"gspe19-cn.ls.apple.com -cn#gspe21-ssl.ls.apple.com -cngspe21.ls.apple.com -cn#gspe35-ssl.ls.apple.com -cn&gspe85-cn-ssl.ls.apple.com -cniadsdk.apple.com -cn, icloud-cdn.icloud.com.akadns.net -cn icloud.cdn-apple.com -cn'images.apple.com.akadns.net -cn?3images.apple.com.edgekey.net.globalredir.akadns.net -cnimages.apple.com -cn3'init-p01md-lb.push-apple.com.akadns.net -cn init-p01md.apple.com -cn3'init-p01st-lb.push-apple.com.akadns.net -cn%init-p01st.push.apple.com -cn3'init-s01st-lb.push-apple.com.akadns.net -cn%init-s01st.push.apple.com -cn!init.itunes.apple.com -cn$iosapps.itunes.apple.com -cn(iosapps.itunes.g.aaplimg.com -cniphone-ld.apple.com -cn'itunes-apple.com.akadns.net -cnitunes.apple.com -cn#itunesconnect.apple.com -cn"js-cdn.music.apple.com -cn km.support.apple.com -cnmaps.apple.com -cn)mesu-cdn.apple.com.akadns.net -cn+mesu-china.apple.com.akadns.net -cnmesu.apple.com -cnmusic.apple.com -cn(ocsp-lb.apple.com.akadns.net -cnocsp.apple.com -cnoscdn.apple.com -cn-!oscdn.origin-apple.com.akadns.net -cn$osxapps.itunes.apple.com -cnpancake.apple.com -cn, pancake.cdn-apple.com.akadns.net -cnpd.itunes.apple.com -cnphobos.apple.com -cn!play.itunes.apple.com -cn1%prod-support.apple-support.akadns.net -cn$se-edge.itunes.apple.com -cn se2.itunes.apple.com -cn#search.itunes.apple.com -cn1%sf-api-token-service.itunes.apple.com -cnsp.itunes.apple.com -cn4(stocks-sparkline-lb.apple.com.akadns.net -cn&stocks-sparkline.apple.com -cn>2store.apple.com.edgekey.net.globalredir.akadns.net -cn'store.apple.com.edgekey.net -cnstore.apple.com -cn2&store.storeimages.apple.com.akadns.net -cn+store.storeimages.cdn-apple.com -cn+streamingaudio.itunes.apple.com -cnsu.itunes.apple.com -cn2&support-china.apple-support.akadns.net -cnsupport.apple.com -cn."swcatalog-cdn.apple.com.akadns.net -cnswcatalog.apple.com -cnswcdn.apple.com -cnswcdn.g.aaplimg.com -cn'swdist.apple.com.akadns.net -cnswdist.apple.com -cn+swscan-cdn.apple.com.akadns.net -cnswscan.apple.com -cn!sync.itunes.apple.com -cn1%updates-http.cdn-apple.com.akadns.net -cn&updates-http.cdn-apple.com -cn upp.itunes.apple.com -cnvalid.apple.com -cn-!valid.origin-apple.com.akadns.net -cn"weather-data.apple.com -cn<0www.apple.com.edgekey.net.globalredir.akadns.net -cn%www.apple.com.edgekey.net -cn www.apple.com -cn#^a[1-5]\.mzstatic\.com$ -cn0$^cdn(-cn)?[1-4]?\.apple-mapkit\.com$ -cn9-^cl[1-5]-cdn\.origin-apple\.com\.akadns\.net$ -cn!^cl[1-5]\.apple\.com$ -cn+^is[1-5](-ssl)?\.mzstatic\.com$ -cn$^s[1-5]?\.mzstatic\.com$ -cnapple.com.akadns.net%!courier-push-apple.com.akadns.netpush-apple.com.akadns.net asus.com asus.com.cn -cn atlassian.com bitbucket.org statuspage.io -trello.com trellocdn.comaccbusiness.com -atandt.comatt-bundles.com att-japan.com att-mail.comatt-promotions.com att-rsvp.comatt.cn -cn att.com att.jobs att.net -att.tvattalascom.comattbelieves.comattbusiness.net attcenter.comattcollaborate.com -attcorp.cn -cnattexperts.com attglobal.netattinnovationspace.comattinternetservice.com -attjoy.comattnetclient.comattonlineoffers.com attproxy.comattpublicpolicy.comattpurchasing.comattsavings.comattspecial.com -attssl.comattstadium.comattsuppliers.com atttvnow.comattuverseoffers.comattuverseonline.com attvoip.com attwifi.comattwirelessinternet.comattwirelessonline.comattwirelesssolutions.com bellsouth.net -budatt.com -cobatt.com currently.com currently.netdirectvbusinessmarket.comenterprisepaging.com itcanwait.com mymmode.comnaturalvoices.comnettyinternet.comopenapiplatform.comopenapiservice.compatentgold.netpaygonline.comraponlinereview.com signalbar.comsundayready.com synaptic.netthanksloyalty.com tvpromise.com -uverse.comwebhosting.com att-idns.net -attdns.com -attdns.netna-att-idns.netaudiencenetwork.comaudiencenetwork.tvcabletvdirectv.com dierectv.com -dirctv.com direcpath.com direcpath.netdirecttv-deals.tvdirecttv-dish.com dkrecttv.com directtv.netdirecttvdeals.tvdirecttvreviews.comdirectv-4-you.comdirectv-newyork.com directv.comdirectvadsales.comdirectvatlantaga.comdirectvboston.comdirectvbundles.comdirectvbusiness.comdirectvcincinnatioh.comdirectvcinema.comdirectvconnect.comdirectvcookevilletn.comdirectvcrossvilletn.comdirectvdealer.comdirectvdeals.comdirectvdealsnow.com directvdsl.tvdirectvforhotels.comdirectvgrandslam.comdirectvhouston.comdirectvinternet.comdirectvkentucky.comdirectvlebanontn.comdirectvlosangeles.comdirectvmetropolisil.comdirectvmonitoring.comdirectvmurfreesborotn.comdirectvnewhampshire.comdirectvnow.comdirectvoffercodes.comdirectvonline.comdirectvplans.comdirectvpomise.comdirectvpromise.comdirectvpromotions.comdirectvrebate.comdirectvrichmond.comdirectvsavings.comdirectvsports.comdirectvsundayticket.com direectv.com -diretv.comdtv2009offers.comfreedirecttvspecial.com getdirect.tv hddirectv.com le-direct.tvmydirectgroove.commydirectvchannels.comredzonechannel.com txdirectv.com  xandr.combandwagonhost.com bwh1.net bwh8.net  bwh88.net canon c-ij.com canon-cee.comcanon-cmos-sensors.com canon-cna.comcanon-ebm.com.hkcanon-emea.comcanon-emirates.aecanon-europa.comcanon-europe.com canon-me.com canon-ois.qacanon-se.com.tw canon.am canon.at canon.az canon.ba canon.be canon.bg canon.ca canon.chcanon.cn -cn canon.co.il canon.co.uk canon.co.za  canon.com canon.com.al canon.com.au canon.com.by canon.com.cn -cn canon.com.cy canon.com.hk canon.com.mk canon.com.mt canon.com.my canon.com.tr canon.com.tw canon.cz canon.de canon.dk canon.ee canon.es canon.fi canon.fr canon.ge canon.gr canon.hr canon.hu canon.ie canon.it canon.kz canon.lt canon.lu canon.lv canon.me  canon.net canon.nl canon.no canon.pl canon.pt canon.ro canon.rs canon.ru canon.se canon.si canon.sk canon.tj canon.ua canon.uzcanonfoundation.orgcanonproprinters.com webex.ca webex.co.in webex.co.it webex.co.jp webex.co.kr webex.co.nz webex.co.uk  webex.com webex.com.au webex.com.br webex.com.cn -cn webex.com.hk webex.com.mx webex.de webex.es webex.fr ciscoacademynetriders.comcciernslabs.comcciesecuritylabs.comccievoicelabs.com  ccna5.netcisco-returns.comcisco-warrantyfinder.comcisco.cn -cn  cisco.com cisco.com.cn -cn -cisco.mobiciscoccservice.comciscoconnectcloud.comciscoconnectcloud.netciscoconnectcloud.orgciscocontest.comciscoerate.com ciscofax.comciscoinvestments.comciscojabbervideo.netciscokinetic.comciscoknowledgenetwork.comciscolearningsociety.orgciscolearningsystem.com ciscolive.comcisconetspace.comcisconetspace.infocisconetspace.netcisconetapp.comciscopartnermarketing.comciscopowercube.comciscoprice.comciscoresearch.comciscosoftware.com ciscospark.caciscospark.com ciscospark.jpciscotaccc.com ciscotr.com ciscoturk.netciscovideo.comciscowebseminars.com cloudlock.com cloupia.com cloupia.netconnect-in-canada.comcoreoptics.net cs.co -devm2m.com dnaspaces.io flipshare.comgpstheseries.com inlethd.cominternetofeverything.com ioe.com kscisco.com m2m.com -mcisco.commediafiles-cisco.com metacloud.com mindmeld.commultiplydiversity.commyciscobenefits.com  mysdn.com -mysdn.info  mysdn.net netacad.com netacad.netnetwork-auth.cn -cnobservable.net obsrvbl.com parstream.com parstream.net parstream.orgservices-exchange.comstatic-cisco.comtandberg-china.com tandberg.com -versly.comvideochampion.com virtuata.comwballiance.com!whyiwantciscotelepresence.com wifi-mx.comyoutubecisco.comapp-dynamics.comappdynamics.co.ukappdynamics.comappdynamics.deappdynamics.frappdynamics.infoappdynamics.jpappdynamics.org -mearki.com meraki-go.com  meraki.hk merakigo.com -tail-f.com  tailf.comcisco.evergage.comciscolivehls-i.akamaihd.net clco.cc -cloudc.one cloudcone.com cloudcone.net -cfblob.comcloudchoose.comcloudvolumes.com -cmpaas.com -cpedge.comcrit-staging.comexperience-vmware.com gemfire.netgeodesummit.com getboxer.com gitstar.com gitstar.net gopivotal.net greenplum.net hwslabs.com immidio.comitfromtheinside.comlatamvmwareforum.compivotalinitiative.comsnapvolumes.com ssdevrd.com vcanedge.com vcloudair.net vfabric.netvidmpreview.com -virsto.com -virsto.net vmglobal.net -vmmark.comvmtestdrive.com vmw.comvmware-cloudmanagement.comvmware-techcenter.comvmware20mosaic.comvmwareausnews.com!vmwarecertificationvideos.comvmwarecloud.comvmwaredemandcenter.comvmwareemeablog.comvmwaregrid.comvmwarehorizon.comvmwareidentity.comvmwarelearning.comvmwarelearningplatform.comvmwarestuff.comvmwareusergroupstore.comvmwareviewpoint.com vmwarevmc.com vmwdemo.com vmwgcomms.comvmwhorizonair.comvmwidm-ads.com -vmwidm.comvmwlabconnect.comvmwlearningplatform.com vmworld.comvmworld2010.comvmworld2013.comvmwsalesrewards.comvmwservices.com -vnware.netwhychoosehorizon.comwhychooseview.comwhychoosevmwareeuc.comwindowsuem.comworkspaceair.com xlstudio.comavinetworks.com -badaas.com bitnami.combitnamiapp.combitnamistudio.comdockerizer.comcloudcredibility.comcloudhealthtech.com cloudlive.comkubeacademy.com kubeapps.com -nyansa.com velocloud.com gopivotal.com heptio.cn -cn -heptio.com aliveipc.com aliveitsm.comaliveprofiler.com alivercm.comaliverewind.com alivertsm.com alivevm.com alivevmax.com alivevue.com vsphere.com vsphere.net air-watch.comairwatchexpress.comairwatchqa.com -getws1.com getwsone.comwhatisairwatch.comwhatisworkspaceone.comworkspaceone.comvmware.tt.omtrdc.netdelldell-brand.com dell.com dellcdn.comaccountkit.comatscaleconference.com botorch.org -buck.build buckbuild.comcomponentkit.org draftjs.org -f8.comfacebookappcenter.infofacebookappcenter.netfacebookappcenter.orgfacebookconnect.comfacebookdevelopergarage.comfaciometrics.com fasttext.cc fbf8.com fbinfer.com fblitho.com fbredex.com -fbrell.com flow.dev flow.org flowtype.org frescolib.org hacklang.org hhvm.commakeitopen.com mcrouter.net mcrouter.orgmessengerdevelopers.com -ogp.meopengraphprotocol.comopengraphprotocol.org  parse.com pyrobot.org  react.com reactjs.com reactjs.org recoiljs.org rocksdb.com rocksdb.net rocksdb.orgyogalayout.comdevelopers.facebook.com!achat-followers-instagram.com#acheter-followers-instagram.com$ acheterdesfollowersinstagram.com!acheterfollowersinstagram.combookstagram.comcarstagram.comcdninstagram.comchickstagram.com ig.me  igcdn.com igsonar.com igtv.com imstagram.com imtagram.cominstaadder.cominstachecker.cominstafallow.cominstafollower.cominstagainer.com instagda.com instagify.cominstagmania.com instagor.cominstagram-brand.cominstagram-engineering.cominstagram-help.cominstagram-press.cominstagram-press.net instagram.cominstagramci.cominstagramcn.cominstagramdi.cominstagramhashtags.netinstagramhilecim.cominstagramhilesi.orginstagramium.cominstagramizlenme.cominstagramkusu.cominstagramlogin.cominstagramm.cominstagramn.cominstagrampartners.cominstagramphoto.cominstagramq.cominstagramsepeti.cominstagramtakipcisatinal.netinstagramtakiphilesi.cominstagramtips.cominstagramtr.com instagran.cominstagranm.com instagrem.com instagrm.cominstagtram.com instagy.cominstamgram.cominstangram.cominstanttelegram.cominstaplayer.net instastyle.tv instgram.com intagram.com intagrm.com intgram.comkingstagram.comlnstagram-help.comtheinstagramhack.comoninstagram.comonline-instagram.comonlineinstagram.comweb-instagram.netwwwinstagram.comfbmessenger.comm.me messenger.com -nbabot.net binoculus.com buyoculus.com ocul.usoculus-china.com -oculus.comoculus2014.com oculus3d.comoculusblog.comoculusbrand.comoculuscasino.net oculuscdn.comoculusconnect.comoculusdiving.comoculusforbusiness.comoculusrift.com oculusvr.compowersunitedvr.com wa.mewhatsapp-plus.infowhatsapp-plus.mewhatsapp-plus.net whatsapp.cc whatsapp.com whatsapp.info whatsapp.net whatsapp.org whatsapp.tvwhatsappbrand.comaboutfacebook.com accessfacebookfromschool.com acebooik.com acebook.comadvancediddetection.comaskfacebook.netaskfacebook.org -atdmt2.com atlasdmt.comatlasonepoint.combuyingfacebooklikes.comcareersatfb.comcelebgramme.comchina-facebook.com click-url.comcomo-hackearfacebook.comcrowdtangle.com dacebook.comdlfacebook.comdotfacebook.comdotfacebook.netexpresswifi.com faacebok.com faacebook.com faasbook.com facbebook.com facbeok.com -facboo.com facbook.com facbool.com facboox.com faccebook.comfaccebookk.com facdbook.com facdebook.com face-book.com faceabook.com facebboc.com facebbook.comfacebboook.com facebcook.com facebdok.com facebgook.com facebhook.com facebkkk.com facebo-ok.com faceboak.com facebock.com facebocke.com facebof.com faceboik.com facebok.comfacebokbook.com facebokc.com facebokk.com facebokok.com faceboks.com facebol.com facebolk.com facebomok.com faceboo.com facebooa.com faceboob.comfaceboobok.com facebooc.com faceboock.com facebood.com facebooe.com faceboof.com facebooi.com facebooik.com facebooik.org facebooj.comfacebook-corp.comfacebook-covid-19.comfacebook-ebook.comfacebook-forum.comfacebook-inc.comfacebook-login.comfacebook-newsroom.comfacebook-newsroom.orgfacebook-pmdcenter.comfacebook-pmdcenter.netfacebook-pmdcenter.orgfacebook-privacy.comfacebook-program.comfacebook-studio.comfacebook-support.orgfacebook-texas-holdem.comfacebook-texas-holdem.net facebook.br facebook.ca facebook.cc facebook.comfacebook.design facebook.net facebook.hu facebook.in fecebook.net facebook.nl facebook.org facebook.se facebook.shop facebook.tv facebook.us facebook.wangfacebook123.orgfacebook30.comfacebook30.netfacebook30.orgfacebook4business.comfacebookads.com"facebookadvertisingsecrets.comfacebookatschool.comfacebookawards.comfacebookblueprint.netfacebookbrand.comfacebookbrand.net3/facebookcanadianelectionintegrityinitiative.comfacebookcareer.comfacebookcheats.comfacebookck.comfacebookclub.comfacebookcom.comfacebookconsultant.orgfacebookcoronavirus.comfacebookcovers.orgfacebookcredits.infofacebookdating.netfacebookdusexe.orgfacebookemail.comfacebookenespanol.comfacebookexchange.comfacebookexchange.netfacebookfacebook.comfacebookflow.comfacebookgames.comfacebookgraphsearch.comfacebookgraphsearch.infofacebookgroups.comfacebookhome.ccfacebookhome.comfacebookhome.infofacebookhub.com facebooki.comfacebookinc.comfacebookland.comfacebooklikeexchange.comfacebooklive.comfacebooklivestaging.netfacebooklivestaging.orgfacebooklogin.comfacebooklogin.infofacebookloginhelp.netfacebooklogs.comfacebookmail.comfacebookmail.tvfacebookmanager.infofacebookmarketing.info facebookmarketingpartner.com!facebookmarketingpartners.comfacebookmobile.comfacebookmsn.comfacebooknews.comfacebooknfl.comfacebooknude.comfacebookofsex.comfacebookook.comfacebookpaper.comfacebookpay.comfacebookphonenumber.netfacebookphoto.comfacebookphotos.comfacebookpmdcenter.comfacebookpoke.netfacebookpoke.orgfacebookpoker.infofacebookpokerchips.infofacebookporn.netfacebookporn.orgfacebookporno.netfacebookportal.com facebooks.comfacebooksafety.comfacebooksecurity.netfacebookshop.comfacebooksignup.netfacebooksite.netfacebookstories.comfacebookstudios.netfacebookstudios.orgfacebooksupplier.comfacebooksuppliers.comfacebookswagemea.comfacebookswagstore.comfacebooksz.comfacebookthreads.netfacebooktv.netfacebooktv.orgfacebookvacation.com facebookw.comfacebookworld.com facebool.com facebool.info facebooll.com faceboom.com faceboon.com faceboonk.comfaceboooik.com faceboook.com faceboop.com faceboot.com faceboox.com facebopk.com facebpook.com facebuk.com facebuok.com facebvook.com facebyook.com facebzook.comfacecbgook.com facecbook.com facecbook.org facecook.com facecook.org facedbook.com faceebok.com faceebook.com faceebot.com facegbok.com facegbook.com faceobk.com faceobok.com faceobook.com faceook.comfacerbooik.com facerbook.com facesbooc.comfacesounds.com facetook.com facevbook.com facewbook.co facewook.comfacfacebook.com facfebook.com fackebook.com facnbook.com facrbook.com facvebook.com facwebook.com facxebook.com fadebook.com -faebok.com faebook.com faebookc.com faeboook.com faecebok.com faesebook.comfafacebook.com faicbooc.com fasebokk.com fasebook.com faseboox.com favebook.com faycbok.com -fb.careers -fb.com fb.me fbacebook.com fbbmarket.comfbboostyourbusiness.com  fbcdn.com  fbcdn.netfbfeedback.com -fbhome.com fbidb.io  fbinc.comfbinnovation.comfbmarketing.com  fbreg.com -fbrpms.com  fbsbx.com  fbsbx.netfbsupport-covid.netfbthirdpartypixel.comfbthirdpartypixel.netfbthirdpartypixel.org  fburl.comfbworkmail.com fcacebook.com fcaebook.com fcebook.com fcebookk.comfcfacebook.comfdacebook.info feacboo.com feacbook.com feacbooke.com feacebook.com fecbbok.com fecbooc.com fecbook.com feceboock.com feceboox.com fececbook.com  feook.com ferabook.com fescebook.com fesebook.com ffacebook.com fgacebook.com ficeboock.com fmcebook.com fnacebook.com fosebook.com fpacebook.com fqcebook.com fracebook.com  freeb.comfreebasics.comfreebasics.net -freebs.comfreefacebook.comfreefacebook.netfreefacebookads.netfreefblikes.comfreindfeed.comfriendbook.info friendfed.comfriendfeed-api.comfriendfeed-media.comfriendfeed.comfriendfeedmedia.com fsacebok.com fscebook.comfundraisingwithfacebook.comfunnyfacebook.orgfutureofbusinesssurvey.org gacebook.com gameroom.comgfacecbook.com -groups.comhackerfacebook.comhackfacebook.comhackfacebookid.comhifacebook.info!howtohackfacebook-account.comhsfacebook.comhttpfacebook.comhttpsfacebook.comhttpwwwfacebook.com i.org internet.org klik.me liverail.com liverail.tvlogin-account.netmarkzuckerberg.commidentsolutions.commobilefacebook.commoneywithfacebook.com myfbfans.com newsfeed.com nextstop.comonline-deals.netopencreate.orgreachtheworldonfacebook.com rocksdb.orgshopfacebook.comsportsfacebook.comsportstream.comsupportfacebook.comterragraph.comthefacebook.comthefacebook.net thefind.comtoplayerserver.comviewpointsfromfacebook.comwhyfacebook.com worldhack.comwww-facebook.comwwwfacebok.comwwwfacebook.comwwwmfacebook.comzuckerberg.comzuckerberg.netfacebookwork.com -redkix.com workplace.comworkplaceusecases.comfbcdn-a.akamaihd.net  aorus.com aoruszone.cn -cn gigabyte.cn -cn gigabyte.comgigabyte2.azureedge.netsa78gs.wpc.edgecastcdn.net"sni1dcb6gl.wpc.edgecastcdn.net godaddy.comx.co godaddy android.comandroidify.com blogspot.ae blogspot.al blogspot.am blogspot.ba blogspot.be blogspot.bg blogspot.bj blogspot.ca blogspot.cf blogspot.ch blogspot.clblogspot.co.atblogspot.co.idblogspot.co.ilblogspot.co.keblogspot.co.nzblogspot.co.ukblogspot.co.za blogspot.comblogspot.com.arblogspot.com.aublogspot.com.brblogspot.com.byblogspot.com.coblogspot.com.cyblogspot.com.eeblogspot.com.egblogspot.com.esblogspot.com.mtblogspot.com.ngblogspot.com.trblogspot.com.uy blogspot.cv blogspot.cz blogspot.de blogspot.dk blogspot.fi blogspot.fr blogspot.gr blogspot.hk blogspot.hr blogspot.hu blogspot.ie blogspot.in blogspot.is blogspot.it blogspot.jp blogspot.kr blogspot.li blogspot.lt blogspot.lu blogspot.md blogspot.mk blogspot.mx blogspot.my blogspot.nl blogspot.no blogspot.pe blogspot.pt blogspot.qa blogspot.re blogspot.ro blogspot.rs blogspot.ru blogspot.se blogspot.sg blogspot.si blogspot.sk blogspot.sn blogspot.td blogspot.tw blogspot.ug blogspot.vn dart.dev dartlang.org dartpad.dev fastlane.cifastlane.tools flutter.devflutterapp.com go-lang.com go-lang.net go-lang.org -go.dev  godoc.org -golang.com -golang.net -golang.org2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -adsscholar.google.aescholar.google.atscholar.google.bescholar.google.bgscholar.google.cascholar.google.catscholar.google.chscholar.google.clscholar.google.cnscholar.google.co.crscholar.google.co.idscholar.google.co.ilscholar.google.co.inscholar.google.co.jpscholar.google.co.krscholar.google.co.nzscholar.google.co.thscholar.google.co.ukscholar.google.co.vescholar.google.co.zascholar.google.comscholar.google.com.arscholar.google.com.auscholar.google.com.boscholar.google.com.brscholar.google.com.coscholar.google.com.cuscholar.google.com.doscholar.google.com.ecscholar.google.com.egscholar.google.com.gtscholar.google.com.hkscholar.google.com.lyscholar.google.com.mxscholar.google.com.myscholar.google.com.nischolar.google.com.pascholar.google.com.pescholar.google.com.phscholar.google.com.pkscholar.google.com.prscholar.google.com.pyscholar.google.com.sgscholar.google.com.svscholar.google.com.trscholar.google.com.twscholar.google.com.uascholar.google.com.uyscholar.google.com.vnscholar.google.czscholar.google.descholar.google.dkscholar.google.esscholar.google.fischolar.google.frscholar.google.grscholar.google.hnscholar.google.hrscholar.google.huscholar.google.isscholar.google.itscholar.google.lischolar.google.ltscholar.google.luscholar.google.lvscholar.google.nlscholar.google.noscholar.google.plscholar.google.ptscholar.google.roscholar.google.ruscholar.google.sescholar.google.sischolar.google.skpolymer-project.orgpolymerproject.org -v8.dev v8project.org youtubeggpht.cn -cn  ggpht.comgooglevideo.comwithyoutube.com youtu.beyoutube-nocookie.com -youtube.ae -youtube.al -youtube.am -youtube.at -youtube.az -youtube.ba -youtube.be -youtube.bg -youtube.bh -youtube.bo -youtube.by -youtube.ca youtube.cat -youtube.ch -youtube.cl -youtube.co youtube.co.ae youtube.co.at youtube.co.cr youtube.co.hu youtube.co.id youtube.co.il youtube.co.in youtube.co.jp youtube.co.ke youtube.co.kr youtube.co.ma youtube.co.nz youtube.co.th youtube.co.tz youtube.co.ug youtube.co.uk youtube.co.ve youtube.co.za youtube.co.zw youtube.comyoutube.com.aryoutube.com.auyoutube.com.azyoutube.com.bdyoutube.com.bhyoutube.com.boyoutube.com.bryoutube.com.byyoutube.com.coyoutube.com.doyoutube.com.ecyoutube.com.eeyoutube.com.egyoutube.com.esyoutube.com.ghyoutube.com.gryoutube.com.gtyoutube.com.hkyoutube.com.hnyoutube.com.hryoutube.com.jmyoutube.com.joyoutube.com.kwyoutube.com.lbyoutube.com.lvyoutube.com.lyyoutube.com.mkyoutube.com.mtyoutube.com.mxyoutube.com.myyoutube.com.ngyoutube.com.niyoutube.com.omyoutube.com.payoutube.com.peyoutube.com.phyoutube.com.pkyoutube.com.ptyoutube.com.pyyoutube.com.qayoutube.com.royoutube.com.sayoutube.com.sgyoutube.com.svyoutube.com.tnyoutube.com.tryoutube.com.twyoutube.com.uayoutube.com.uyyoutube.com.ve -youtube.cr -youtube.cz -youtube.de -youtube.dk -youtube.ee -youtube.es -youtube.fi -youtube.fr -youtube.ge -youtube.gr -youtube.gt -youtube.hk -youtube.hr -youtube.hu -youtube.ie -youtube.in -youtube.iq -youtube.is -youtube.it -youtube.jo -youtube.jp -youtube.kr -youtube.kz -youtube.la -youtube.lk -youtube.lt -youtube.lu -youtube.lv -youtube.ly -youtube.ma -youtube.md -youtube.me -youtube.mk -youtube.mn -youtube.mx -youtube.my -youtube.ng -youtube.ni -youtube.nl -youtube.no -youtube.pa -youtube.pe -youtube.ph -youtube.pk -youtube.pl -youtube.pr -youtube.pt -youtube.qa -youtube.ro -youtube.rs -youtube.ru -youtube.sa -youtube.se -youtube.sg -youtube.si -youtube.sk -youtube.sn youtube.soy -youtube.sv -youtube.tn -youtube.tv -youtube.ua -youtube.ug -youtube.uy -youtube.vnyoutubeeducation.comyoutubegaming.comyoutubego.co.idyoutubego.co.in youtubego.comyoutubego.com.br youtubego.id youtubego.inyoutubei.googleapis.comyoutubekids.comyoutubemobilesupport.com yt.be  ytimg.comand -chromedclkgbizgle gmailgoogoog -googleguge hangout nexus xn--qcka1pmc  google.ad  google.ae  google.al  google.am  google.as  google.at  google.az  google.ba  google.be  google.bf  google.bg  google.bi  google.bj  google.bs  google.bt  google.by  google.ca -google.cat  google.cd  google.cf  google.cg  google.ch  google.ci  google.cl  google.cm google.cn -cn google.co.ao google.co.bw google.co.ck google.co.cr google.co.id google.co.il google.co.in google.co.jp google.co.ke google.co.kr google.co.ls google.co.ma google.co.mz google.co.nz google.co.th google.co.tz google.co.ug google.co.uk google.co.uz google.co.ve google.co.vi google.co.za google.co.zm google.co.zw -google.com google.com.af google.com.ag google.com.ai google.com.ar google.com.au google.com.bd google.com.bh google.com.bn google.com.bo google.com.br google.com.bz google.com.co google.com.cu google.com.cy google.com.do google.com.ec google.com.eg google.com.et google.com.fj google.com.gh google.com.gi google.com.gt google.com.hk google.com.jm google.com.kh google.com.kw google.com.lb google.com.ly google.com.mm google.com.mt google.com.mx google.com.my google.com.na google.com.ng google.com.ni google.com.np google.com.om google.com.pa google.com.pe google.com.pg google.com.ph google.com.pk google.com.pr google.com.py google.com.qa google.com.sa google.com.sb google.com.sg google.com.sl google.com.sv google.com.tj google.com.tr google.com.tw google.com.ua google.com.uy google.com.vc google.com.vn  google.cv  google.cz  google.de  google.dj  google.dk  google.dm  google.dz  google.ee  google.es  google.fi  google.fm  google.fr  google.ga  google.ge  google.gg  google.gl  google.gm  google.gr  google.gy  google.hn  google.hr  google.ht  google.hu  google.ie  google.im  google.iq  google.is  google.it  google.je  google.jo  google.kg  google.ki  google.kz  google.la  google.li  google.lk  google.lt  google.lu  google.lv  google.md  google.me  google.mg  google.mk  google.ml  google.mn  google.ms  google.mu  google.mv  google.mw  google.ne  google.nl  google.no  google.nr  google.nu  google.pl  google.pn  google.ps  google.pt  google.ro  google.rs  google.ru  google.rw  google.sc  google.se  google.sh  google.si  google.sk  google.sm  google.sn  google.so  google.sr  google.st  google.td  google.tg  google.tl  google.tm  google.tn  google.to  google.tt  google.vg  google.vu  google.ws 0emm.com  1e100.net  1ucrs.com -466453.com abc.xyz adgoogle.net -admeld.comangulardart.org -api.ai -apigee.com appbridge.ca appbridge.io appbridge.it appspot.com -apture.com area120.com -asp-cc.com autodraw.com bandpage.combaselinestudy.combaselinestudy.org bazel.build bdn.devbeatthatquote.com  blink.org blogblog.com blogger.combrocaproject.com -brotli.org bumpshare.com -bumptop.ca bumptop.com bumptop.net bumptop.org bumptunes.comcampuslondon.com certificate-transparency.org chat.gle -chrome.comchromebook.comchromecast.com chromeos.dev chromium.orgcloudburstresearch.comcloudfunctions.netcloudrobotics.comcobrasearch.com codespot.com conscrypt.com conscrypt.orgcookiechoices.org  coova.com  coova.net  coova.orgcrossmediapanel.com crr.com  cs4hs.comdartsearch.netdataliberation.org  debug.comdebugproject.com deepmind.comdevsitetest.howdialogflow.comdigitalassetlinks.org episodic.comfeedburner.com -fflick.comfinanceleadsonline.comfirebaseapp.comfirebaseio.com -foofle.com froogle.com fuchsia.dev  g-tun.comg.cn -cng.co g.dev -g.pagegerritcodereview.comgetbumptop.com ggoogle.com gipscorp.com gkecnapps.cn -cn globaledu.org  gmail.com gmodules.com  gogle.com -gogole.comgonglchuangl.net -goo.gl -googel.com -googil.com  googl.comgoogle-syndication.com google.berlin -google.dev -google.net -google.orggoogle.ventures"googleacquisitionmigration.com googleapis.cn -cngoogleapis.comgoogleapps.comgooglearth.comgoogleblog.com googlebot.comgooglecapital.comgooglecert.netgooglecnapps.cn -cngooglecode.comgooglecommerce.comgooglecompare.co.ukgoogledanmark.comgoogledomains.comgoogledrive.com googlee.comgoogleearth.comgooglefiber.netgooglefinland.comgooglemail.comgooglemaps.comgooglepagecreator.comgooglephotos.comgoogleplay.comgoogleplus.comgooglesource.comgooglestore.comgooglesverige.comgoogleusercontent.comgoogleventures.com -googlr.com -goolge.com gooogle.com gridaware.app gsrc.io -gstatic.cn -cn gstatic.comgstaticcnapps.cn -cn -gsuite.com -gv.com gvt0.com gvt1.com gvt2.com gvt3.com gvt5.com gvt6.com gvt7.com gvt9.comhdrplusdata.org hey.gle hindiweb.comhowtogetmo.co.ukhtml5rocks.com hwgo.comiamremarkable.org igoogle.com impermium.com -j2objc.orgjibemobile.com keyhole.comkeytransparency.comkeytransparency.fookeytransparency.org lanternal.com like.commadewithcode.com material.io mdialog.com meet.newmfg-inspector.commobileview.pagemoodstocks.com near.by nest.com nomulus.foo -oauthz.com -ok.gle on.here on2.comonefifteen.netonefifteen.orgoneworldmanystories.com openthread.io openweave.io orbitera.com  page.linkpagespeedmobilizer.com pageview.mobi panoramio.compartylikeits1986.orgpaxlicense.org -picasa.com picasaweb.com picasaweb.net picasaweb.org -picnik.com pittpatt.com postini.comprojectara.comprojectbaseline.compublishproxy.comquestvisual.comquickoffice.com quiksee.com -revolv.comridepenguin.com run.appsavethedate.foo -saynow.com schemer.comscreenwisetrends.comscreenwisetrendspanel.com snapseed.com solveforx.com -stadia.devstcroixmosquito.comstcroixmosquitoproject.comstudywatchbyverily.comstudywatchbyverily.orgstxmosquito.comstxmosquitoproject.comstxmosquitoproject.netstxmosquitoproject.org synergyse.comtensorflow.org  tfhub.devthecleversense.comthegooglestore.comthinkquarterly.co.ukthinkquarterly.comthinkwithgoogle.com tiltbrush.com txcloud.net  txvia.comunfiltered.news useplannr.comusvimosquito.comusvimosquitoproject.comvelostrata.com -verily.comverilylifesciences.comverilystudyhub.comverilystudywatch.comverilystudywatch.org -wallet.com  waymo.com waze.com web.app web.devwebappfieldguide.comwebmproject.orgwebpkgcache.com -webrtc.orgweltweitwachsen.dewhatbrowser.org widevine.comwithgoogle.com womenwill.comwomenwill.com.br womenwill.id womenwill.in womenwill.mx  x.company -x.team xn--9kr7l.comxn--9trs65b.comxn--flw351e.com xplr.cozukunftswerkstatt.de265.com -cn2mdn.net -cn adservice.google.com -cnapp-measurement.com -cn beacons.gcp.gvt2.com -cnbeacons.gvt2.com -cnbeacons3.gvt2.com -cn c.admob.com -cn(c.android.clients.google.com -cn!cache.pack.google.com -cncheckin.gstatic.com -cn%clickserve.dartsearch.net -cn)clientservices.googleapis.com -cn)connectivitycheck.gstatic.com -cncorp.google.com -cn crl.pki.goog -cn"csi-china.l.google.com -cncsi.gstatic.com -cn dl.google.com -cndl.l.google.com -cndoubleclick.net -cn-!firebase-settings.crashlytics.com -cn fonts.googleapis.com -cnfonts.gstatic.com -cn google-analytics.com -cn googleadservices.com -cngoogleanalytics.com -cn!googlesyndication.com -cn googletagmanager.com -cn!googletagservices.com -cngtm.oasisfeng.com -cn!imasdk.googleapis.com -cn kh.google.com -cn ocsp.pki.goog -cn, pagead-googlehosted.l.google.com -cn!pki-goog.l.google.com -cn recaptcha.net -cnredirector.gvt1.com -cn)safebrowsing-cache.google.com -cn'safebrowsing.googleapis.com -cn$settings.crashlytics.com -cn-!ssl-google-analytics.l.google.com -cnssl.gstatic.com -cn%toolbarqueries.google.com -cntools.google.com -cntools.l.google.com -cn$translate.googleapis.com -cn!update.googleapis.com -cn-!www-googletagmanager.l.google.com -cnwww.gstatic.com -cnwww.recaptcha.net -cn0$^khm([0-3]|db)?\.google(apis)?\.com$ -cn  hinet.net -123-hp.com123hplaserjet.com8008206616.com as-hp.cabaterias-hp.com campushp.comcolorprotechnology.com -compaq.orgdavid-laserscanner.com demoprint.comdigitalpack.com eprintsw.comgaming-notebooks.comgetprintersupports.comhp-imagine.comhp-infolab.com hp-invent.comhp-invent.info -hp-mns.comhp-printing.comhp-webplatform.com  hp-ww.com -hp.com -hp.company hp.io hp3d.comhp3dmetals.comhp3dprinter.comhp3dsamplepromo.comhpallinoneprinter.comhpapplicationscenter.com hpbestbuy.comhpbluecarpet.comhpbluecarpet.nethpbuiltforlearning.com hpbundle.com hpcampus.comhpcatridge.com  hpccp.com  hpceo.com  hpcmw.nethpcodewarsbcn.comhpcomputers.comhpcomputerservices.comhpconnected.comhpconnected.nethpconnected.orghpconnected.ushpconnectedstage.comhpcontinuum.com  hpcpi.com hpcu.orghpcustomersupport.comhpcustomersupport.net -hpdaas.comhpdesignjetl25500.comhpdesktopcomputer.comhpdownloadstore.comhpdreamcolor.com hpdriver.com hpdrivers.com hpdrivers.org hpeclipse.com hpengage.com hpeprint.comhpeprintcenter.com hpeurope.comhpeventcenter.com -hpfaqs.comhpfeedback.com -hpgift.com -hpgpas.comhpgraphicspartners.com  hpiie.org -hpinc.info  hpinc.nethpindigopress.com hpinkjet.comhpinstantink.cahpinstantink.comhpkeyboard.com hplaptop.comhplaptopbattery.comhplargeformatremote.comhplatexknowledgecenter.com hplfmedia.com  hplip.nethplipopensource.comhpmarketplace.comhpmemorychips.comhpmicrcartridge.com -hpmini.com hpmobile.comhpofficejetprinter.comhponlinehelp.comhponlineprinting.comhppage5000.comhppavillionlaptop.comhpphotoscanners.comhpprinterdrivers.comhpprinterinstalls.comhpprintersupplies.comhpsalescentral.com hpserver.comhpservices.comhpshooping.com -hpshop.com hpshoping.com hpshopping.hk hpsignage.com hpsmart.com hpsmarts.comhpsmartstage.comhpsmartupdate.comhpsoftware.com hpspeaker.comhpsprocket.com  hpsso.comhpstore-china.comhpstore-emea.com -hpstore.cn -cn hpstore.comhpstorethailand.com hpsupport.comhpsuresupply.comhptechventures.com hpto.net hptouch.comhptouchpointmanager.comhptrainingcenter.com  hpuae.comhpuniversity.infohpusertraining.com -hpveer.comhpvirtualthin.com hpwallart.comhpwellnesscentral.com  hpwsn.com i-123-hp.com inkchoice.cominstantink.comleavinghpinc.comlife-global.orgmyhpsupport.com printeron.comprintersetupsupport.comprintspots.comprintspots.netserviceshp.comthecompaqstore.comtouchsmartpc.comtouchsmartpc.nettouchsmartpc.orgworkstations.tv www8-hp.comxn--6eup7j.comxn--6eup7j.nethpstore.corpmerchandise.comibmweatherchannel weather bluemix.com bluemix.net ibm.com -ibm.eu ibm.net -ibm.us s-bluemix.net s81c.com -01.org hyperscan.iointellinuxgraphics.comintellinuxgraphics.net -oneapi.comopenvinotoolkit.orgsnap-telemetry.io -acpica.com clearlinux.cn -cnclearlinux.org -ospray.net -ospray.org intel intel.ac intel.ae intel.af intel.ag intel.ai intel.ar intel.at intel.az intel.ba intel.bg intel.bh intel.bi intel.bo intel.bs intel.by intel.ca intel.cc intel.cg intel.ch intel.cl intel.cmintel.cn -cn intel.co.ae intel.co.cr intel.co.id intel.co.il intel.co.jp intel.co.kr intel.co.uk intel.co.za  intel.com intel.com.ar intel.com.au intel.com.bo intel.com.br intel.com.cn -cn intel.com.co intel.com.ec intel.com.hk intel.com.jm intel.com.mx intel.com.my intel.com.pe intel.com.ph intel.com.pr intel.com.py intel.com.tr intel.com.tw intel.com.uy intel.com.ve intel.cr intel.cu intel.cz intel.de intel.dk intel.dz intel.ec intel.ee intel.eg intel.es intel.eu intel.fi intel.fr intel.ga intel.gd intel.ge intel.gl intel.gm intel.gr intel.gs intel.gt intel.gy intel.hk intel.hn intel.ht intel.hu intel.ie intel.in intel.io intel.it intel.je intel.jo intel.jp intel.ke intel.la intel.lc intel.lk intel.lt intel.lu intel.ly intel.ma intel.md intel.me intel.mg intel.mk intel.mn intel.mp intel.mt intel.mu intel.mw intel.mx intel.my intel.ng intel.nl intel.nu intel.nz intel.pa intel.pe intel.ph intel.pl intel.pn intel.re intel.ro intel.ru intel.rw intel.sa intel.sc intel.se intel.sg intel.si intel.sk intel.sn intel.sr intel.st intel.sv intel.sx intel.sy intel.tf intel.tj intel.tl intel.tm intel.tn intel.tt intel.tv intel.tw intel.ua intel.uk intel.us intel.uy intel.uz intel.vg intel.vn intel.vu intel.wf intel.yt -altera.comalteraforum.comalteraforums.comalteraforums.netalterauserforum.comalterauserforum.netalterauserforums.comalterauserforums.net buyaltera.com celeron.com celeron.net centrino.com centrino.net  chips.com cilk.com cilk.netcloudinsights.comclusterconnection.com coreduo.comcoreextreme.comcrosswalk-project.comcrosswalk-project.netdoceapower.com  easic.com enpirion.comexascale-tech.comexploreintel.comgordonmoore.cominsidefilms.com intc.comintel-research.net&"intel-university-collaboration.netintelamericasstore.comintelapacstore.com intelatom.netintelcapital.comintelcapital.netintelcloudbuilders.comintelcloudfinder.comintelemeastore.cominteleventexpress.comintelforchange.comintelfreepress.com intelgo.netintelinsight.cominteliotmarketplace.com -intell.comintellearningseries.comintellinuxwireless.netintelnervana.comintelnet.component intelplay.comintelquark.comintelrealsense.cn -cnintelrealsense.com intelrxt.comintelsalestraining.comintelsecurity.comintelserveredge.comintelsoftwarenetwork.comintelstore.cominteltechnologyprovider.com intelvmwarecybersecurity.com  itnel.comlatencytop.comlookinside.commakebettercode.commakesenseofdata.com movidius.com movidius.netnervanasys.com  nevex.comnextgenerationcenter.com -niosii.com -niosii.netomekinteractive.com -omnitek.tv openamt.comopendroneid.orgoptanedifference.com -pc.com pentium.com pentium.net pintool.compoweredbyintel.comreconinstruments.com reconjet.comresearchintel.comsaffrontech.comsensorynetworks.com -siport.comsmart-edge.comtheintelstore.comthunderbolttechnology.nettrustedanalytics.comtrustedanalytics.net -vokevr.com vpro.com vpro.net xeon.comxn--ztsq84g.cn -xscale.combarefootnetworks.com linkedin.com  licdn.com logi.comlogicool.co.jp logicoolg.com logitech.biz logitech.com logitech.fr logitechg.com logitechg.frworldsfastestgamer.net azure-dns.comazure-dns.info azure-dns.net azure-dns.orgazure-mobile.netazure-sphere.comazure-test.netazure.cn -cn  azure.com  azure.netazurecomcdn.netazurecontainer.ioazurecosmos.netazurecosmosdb.comazurecosmosdb.infoazurecosmosdb.netazuredatabricks.netazuredevopslaunch.comazuredigitaltwin.comazuredigitaltwins.comazuredigitaltwins.netazuredns-prd.infoazuredns-prd.orgazureedge-test.net azureedge.netazureiotcentral.comazureiotsolutions.comazureiotsuite.comazureplanetscale.infoazureplanetscale.netazureserviceprofiler.comazuresmartspaces.netazurestackvalidation.comazurewebsites.netchinacloudapi.cn -cnchinacloudapp.cn -cnchinacloudsites.cn -cn cosmosdb.info cosmosdb.net devopsms.com gotcosmos.commicrosofteca.commicrosoftiotcentral.comserverlesslibrary.nettryfunctions.comwindowsazure.cn -cnwindowsazure.comazure.microsoft.com"azuremarketplace.microsoft.combing bing.com bing.com.cn -cn bing.net bingads.combingagencyawards.combingapistatistics.combingsandbox.combingvisualsearch.com bingworld.combluehatnights.com -dictate.msflipwithsurface.commasalladeloslimites.commicrosoft-give.commicrosoftcloudsummit.commicrosoftdiplomados.commicrosoftlatamholiday.commicrosoftmxfilantropia.com!microsoftpartnersolutions.commsunlimitedcloudsummit.comoffice365love.comoffice365tw.comrenovacionoffice.comsprinklesapp.com cn.bing.com -cn cn.bing.net -cn ditu.live.com -cnbj1.api.bing.com -cnemoi-cncdn.bing.com -cn atom.io  npmjs.com npm.communitydependabot.com -git.io github.blog -github.comgithub.community  github.io githubapp.comgithubassets.comgithubhackathon.comgithubstatus.comgithubusercontent.com/+github-avatars.oss-cn-hongkong.aliyuncs.comFBgithub-production-release-asset-[0-9a-za-z]{6}\.s3\.amazonaws\.comadaptivecards.ioapi-extractor.com -apisof.net appcenter.ms -blazor.netbotframework.comcodethemicrobit.comdevopsassessment.net dot.netgamesstack.comgraphengine.ioinsiderdevtour.com -jwt.ms microbit.orgmicrosoftadc.commicrosoftgamestack.commicrosoftiotinsiderlabs.commicrosoftreactor.cn -cn#microsoftreactor.com.cn -cnmicrosoftreactor.infomicrosoftreactor.netmicrosoftreactor.orgmicrosoftsilverlight.commicrosoftsilverlight.orgmicrosoftsilverlight.netmicrosoftsqlserver.com  mmdnn.commono-project.com msdn.commsinnovationchallenge.commspairlift.commsropendata.com  nuget.org nugettest.orgopentranslatorstothings.orgposhtestgallery.compwabuilder.comreactorms.com.cn -cn -sankie.netsqlserveronlinux.comtimelinestoryteller.comuwpcommunitytoolkit.comvisualstudio-staging.comvisualstudio.comvisualstudio.covisualstudio.co.ukvisualstudio.euvisualstudio.net vfsforgit.com vfsforgit.org vsassets.io xamarin.comdeveloper.microsoft.comdtlgalleryint.cloudapp.net poshtestgallery.cloudapp.net"psg-int-centralus.cloudapp.netpsg-int-eastus.cloudapp.netmicrosoftnews.ccmicrosoftnews.cn -cnmicrosoftnews.commicrosoftnews.netmicrosoftnews.orgmicrosoftnewsforkids.commicrosoftnewsforkids.netmicrosoftnewsforkids.orgmicrosoftnewskids.commicrosoftnewskids.netmicrosoftnewskids.org msn.commsnewskids.commsnewskids.netmsnewskids.org msnkids.com 1drv.com 1drv.mslivefilestore.com onedrive.coonedrive.co.uk onedrive.com onedrive.eu onedrive.net onedrive.orgonedrive.live.comstorage.live.comcallersbane.com minecraft.netminecraftshop.com -mojang.comforzamotorsport.netforzaracingchampionship.com forzarc.comorithegame.comrenovacionxboxlive.comtellmewhygame.com xbox.co xbox.com xbox.eu xbox.org -xbox360.co xbox360.com -xbox360.eu xbox360.org -xboxab.comxboxgamepass.comxboxgamestudios.com xboxlive.cn -cn xboxlive.com -xboxone.co xboxone.com -xboxone.euxboxplayanywhere.comxboxstudios.com  microsoft azure hotmail -office skype windowsxbox microsoft.az microsoft.be microsoft.by microsoft.ca microsoft.cat microsoft.ch microsoft.cl microsoft.com microsoft.cz microsoft.de microsoft.dk microsoft.ee microsoft.es microsoft.eu microsoft.fi microsoft.ge microsoft.hu microsoft.io microsoft.is microsoft.it microsoft.jp microsoft.lt microsoft.lu microsoft.lv microsoft.md microsoft.pl microsoft.pt microsoft.red microsoft.ro microsoft.rs microsoft.ru microsoft.se microsoft.si microsoft.tv microsoft.ua microsoft.uz microsoft.vnfemalefounderscomp.com -m12.vc -aka.msankarazirvesi2018.comapplicationinsights.ioapplicationinsights.net aria.ms asp.net aspnetcdn.com -binads.com bluehatil.combrazilpartneruniversity.com breakdown.mecentralvalidation.com -ch9.mscharticulator.com cloudapp.netcloudappsecurity.comcortanaanalytics.comcortanaskills.comcrmdynint-gcc.com crmdynint.comcrossborderexpansion.com docs.com dynamics.com efproject.netazuredigitaltwin.comfasttrackreadysupport.comfluidpreview.comgameuxmasterguide.com -gears5.com gearspop.comgearstactics.com -gfx.ms -gigjam.com hololens.com -hotmail.co hotmail.com -hotmail.eu hotmail.net hotmail.orghummingbird.ms -ie8.co ie9.com ie10.com ie11.cominternetexplorer.cointernetexplorer.com imaginecup.pl -ingads.comintunewiki.comiotinactionevents.com -kidgrid.tvlatampartneruniversity.com live.com live.com.au live.eu live.netlivingyourambition.com makecode.orgmanagedmeetingrooms.commeetfasttrack.commeetyourdevices.com mepn.commicrosoft-int.commicrosoft-ppe.commicrosoft-sap-events.commicrosoft-sbs-domains.commicrosoft-smb.cn -cnmicrosoft365.commicrosoftaccountguard.commicrosoftads.commicrosoftadvertising.com*µsoftadvertisingregionalawards.commicrosoftaffiliates.commicrosoftcloud.commicrosoftcloudworkshop.com"microsoftcommunitytraining.commicrosoftedge.commicrosofthouse.commicrosofthouse.netmicrosoftinternetsafety.netmicrosoftlatamaitour.commicrosoftlinc.commicrosoftonline-p.commicrosoftonline.com!microsoftpartnercommunity.commicrosoftready.commicrosoftsiteselection.commicrosoftstore.commicrosoftstore.com.hkmicrosoftstream.commicrosoftteams.commicrosofttradein.commicrosofttranslator.commicrosoftuwp.commomentumms.commorphcharts.commpnevolution.comms-studiosmedia.comms365surfaceoffer.com  msads.net msauth.cn -cn -msauth.netmschallenge2018.com -msecnd.net -msedge.net  msft.info msft.net msftauth.cn -cn msftauth.netmsfteducation.ca msftnet.orgmsgamesresearch.com -msocdn.com -msocsp.com msturing.orgmsudalosti.commymicrosoft.comnextechafrica.net nxta.orgo365weve-dev.como365weve-ppe.com o365weve.com -office.com -office.net office365.com onenote.com onenote.net onestore.msopticsforthecloud.netoutingsapp.com outlook.com passport.net -pixapp.netpowerappscdn.netpowerautomate.com powerbi.compowershellgallery.comprojectmurphy.netprojectsangam.com -pxt.ios-microsoft.com  s-msn.com sfbassets.com sfbassets.net -sfx.ms sharepoint.cn -cnsharepoint.comsharepointonline.com  skype.com  skype.netskypeassets.comskypeassets.net staffhub.mssuccesswithteams.com surface.com sway-cdn.com sway.comsysinternals.comtailwindtraders.com -techhub.ms tfsallin.nettrafficmanager.nettranslatetheweb.comunlocklimitlesslearning.com -userpxt.iovideobreakdown.comvideoindexer.aivirtualearth.net vsallin.net -wbd.ms what-fan.netwindows-int.net windows.com windows.net -windows.nl windows8.hkwindowscommunity.netwindowsmarketplace.comwindowsphone-int.comwindowsphone.comwindowssearch.comwindowsupdate.com -winhec.com -winhec.net -yammer.comcdn.office.net -cn(dl.delivery.mp.microsoft.com -cn&download.windowsupdate.com -cn'bg.v4.a.dl.ws.microsoft.com -cn(bg4.v4.a.dl.ws.microsoft.com -cn2&cdn.marketplaceimages.windowsphone.com -cn cn.windowssearch.com -cn#ctldl.windowsupdate.com -cn#developer.microsoft.com -cndocs.microsoft.com -cn"download.microsoft.com -cn/#download.visualstudio.microsoft.com -cnfs.microsoft.com -cn(msgr.dlservice.microsoft.com -cn, msgruser.dlservice.microsoft.com -cn#officecdn.microsoft.com -cn&qh.dlservice.microsoft.com -cnsdx.microsoft.com -cn/#silverlight.dlservice.microsoft.com -cn, slupdate.dlservice.microsoft.com -cn, storeedgefd.dsx.mp.microsoft.com -cn&wl.dlservice.microsoft.com -cn&wscont1.apps.microsoft.com -cn&wscont2.apps.microsoft.com -cnwww.microsoft.com -cn&xb.dlservice.microsoft.com -cn/+img-prod-cms-rt-microsoft-com.akamaized.netimg-s-msn-com.akamaized.netmwf-service.akamaized.net)%officecdn-microsoft-com.akamaized.net3/statics-marketingsites-eus-ms-com.akamaized.net40statics-marketingsites-wcus-ms-com.akamaized.net firefox.comdeveloper.mozilla.org($interactive-examples.mdn.mozilla.netmdn.mozillademos.orgmedia.prod.mdn.mozit.cloud rust-lang.org  rustup.rs  crates.io docs.rsmozilla.community mozilla.net mozilla.org bugzilla.org getpocket.comseamonkey-project.orgthunderbird.net -lin.ee line.me line.naver.jp line-apps.com line-cdn.net line-scdn.net -nhncorp.jp grafolio.com  naver.com naver.jp  naver.net navercorp.com  plug.game pstatic.net neuralink.com -geforce.cn -cn geforce.co.kr geforce.co.uk geforce.comgeforce.com.twgputechconf.cn -cngputechconf.co.krgputechconf.comgputechconf.com.augputechconf.com.twgputechconf.eugputechconf.ingputechconf.jp nvidia.asia  nvidia.be  nvidia.ch nvidia.cn -cn nvidia.co.jp nvidia.co.kr -nvidia.com nvidia.com.au nvidia.com.br nvidia.com.tr nvidia.com.tw  nvidia.cz  nvidia.de  nvidia.dk  nvidia.es  nvidia.eu  nvidia.fi  nvidia.fr  nvidia.it  nvidia.lu  nvidia.nl  nvidia.no  nvidia.pl  nvidia.ro  nvidia.ru  nvidia.seshotwithgeforce.com -oracle -oracle.comoraclecloud.comoraclefoundation.org oracleimg.comateam-oracle.com -bronto.comcovid19-rx.org covid19rx.org sun.com addthis.comaddthiscdn.comaddthisedge.comjava java.com java.net  razer.comrazersupport.com razerzone.com razerzone.jprespawnbyrazer.com appcloud.comappexchange.com attic.iobeyondcore.combuddymedia.com chatter.comcloudcraze.com cotweet.com data.comdemandware.com desk.comdocumentforce.comdreamforce.com einstein.comexacttarget.com  force.com govforce.comgravitytank.com heywire.com krux.commarketingcloud.com metamind.io -pardot.com quotable.com radian6.com relateiq.comsalesforce.comsalesforce.orgsalesforceiq.comsalesforceliveagent.com salesforcemarketingcloud.com sequence.comsfdcstatic.com -sforce.com site.com -social.comsteelbrick.com twinprime.comweinvoiceit.com samsung -xn--cg4bkigalaxyappstore.comgalaxymobile.jpgame-platform.net knoxemm.com samsung.comsamsungapps.comsamsungcloud.com samsungdm.comsamsungeshop.com.cn -cnsamsunggalaxyfriends.comsamsungknox.comsamsungqbe.com playstationplaystation.complaystation.net sonyentertainmentnetwork.comimageworks.commotionpictureser.com playshow.ioplayshowtv.comsonyclassics.comsonypictures.comsonypicturesanimation.comsonypicturesmuseum.comsonypicturesstudios.comsonypicturestelevision.com#sonypicturestelevisiongames.com wheelpop.com -wwtbam.comyourfantasybeginsnow.comsonyginzasonypark.jpsony-africa.com sony-asia.comsony-europe.comsony-hes.co.jpsony-latin.com sony-mea.comsony-olympus-medical.comsony-promotion.eusony-semicon.co.jp sony.at sony.ba sony.be sony.bg sony.ca sony.ch sony.cl -sony.co.cr -sony.co.id -sony.co.in -sony.co.jp -sony.co.kr -sony.co.nz -sony.co.th -sony.co.uk sony.com sony.com.ar sony.com.au sony.com.bo sony.com.br sony.com.co sony.com.do sony.com.ec sony.com.gt sony.com.hk sony.com.hn sony.com.mk sony.com.mx sony.com.my sony.com.ni sony.com.pa sony.com.pe sony.com.ph sony.com.sg sony.com.sv sony.com.tr sony.com.tw sony.com.vn sony.cz sony.de sony.dk sony.ee sony.es sony.eu sony.fi sony.fr sony.gr sony.hr sony.hu sony.ie sony.it sony.kz sony.lt sony.lu sony.lv sony.net sony.nl sony.no sony.pl sony.pt sony.ro sony.rs sony.ru sony.se sony.si sony.sk sony.ua sonybo.co.jp sonybsc.comsonybuilding.jpsonydesign.com sonydna.comsonyglobalsolutions.jp sonykigyo.jpsonylatvija.com -sonypcl.jpsonyprotechnosupport.co.jpsonystoragemedia.co.jp sonytc.co.jp  sovec.net -spacex.comboringcompany.com x.comads-twitter.com periscope.tv pscp.tvt.co tellapart.com tweetdeck.com  twimg.com twitpic.com twitter.com -twitter.jptwitterinc.comtwitteroauth.comtwitterstat.us twtrdns.net vine.couber-assets.com uber.com ubereats.com -oath.cloud oath.com ouroath.comstatic-verizon.com verizon.com verizon.netverizonbusinessfios.comverizonenterprise.comverizonfios.comverizonmedia.comverizonwireless.com vzw.com -xo.com mvk.com userapi.com  vk-cdn.me vk-portal.net vk.cc -vk.com  vk.design vk.link vk.me vkcache.com vkgo.app -vklive.appvkmessenger.comvkmessenger.app vkontakte.ruvkuseraudio.comvkuserlive.netvkuservideo.comvkuservideo.netg-technology.com -sandisk.ae -sandisk.it -sandisk.cn -cn sandisk.co.jp sandisk.co.kr sandisk.co.uk sandisk.comsandisk.com.ausandisk.com.brsandisk.com.trsandisk.com.tw -sandisk.de -sandisk.es -sandisk.fr -sandisk.hk -sandisk.id -sandisk.in -sandisk.nl -sandisk.sg -wd.com wdc.comwesterndigital.com -flickr -flickr.com -flickr.netstaticflickr.com ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -ads yahoo yahoo.ae yahoo.am yahoo.as yahoo.at yahoo.az yahoo.ba yahoo.be yahoo.bg yahoo.bi yahoo.bs yahoo.bt yahoo.by yahoo.ca  yahoo.cat yahoo.cd yahoo.cg yahoo.ch yahoo.cl yahoo.cmyahoo.cn -cn yahoo.co.ao yahoo.co.bw yahoo.co.ck yahoo.co.cr yahoo.co.id yahoo.co.il yahoo.co.in yahoo.co.jp yahoo.co.kr yahoo.co.mz yahoo.co.nz yahoo.co.th yahoo.co.tz yahoo.co.uk yahoo.co.uz yahoo.co.ve yahoo.co.vi yahoo.co.za  yahoo.com yahoo.com.af yahoo.com.ag yahoo.com.ai yahoo.com.ar yahoo.com.au yahoo.com.bd yahoo.com.bo yahoo.com.br yahoo.com.bz yahoo.com.cn -cn yahoo.com.co yahoo.com.do yahoo.com.ec yahoo.com.eg yahoo.com.es yahoo.com.fj yahoo.com.gi yahoo.com.gt yahoo.com.hk yahoo.com.kw yahoo.com.lb yahoo.com.ly yahoo.com.mt yahoo.com.mx yahoo.com.my yahoo.com.na yahoo.com.nf yahoo.com.om yahoo.com.pa yahoo.com.pe yahoo.com.ph yahoo.com.pk yahoo.com.pr yahoo.com.py yahoo.com.sa yahoo.com.sb yahoo.com.sg yahoo.com.sv yahoo.com.tj yahoo.com.tr yahoo.com.tw yahoo.com.ua yahoo.com.uy yahoo.com.vc yahoo.com.vn yahoo.cz yahoo.de yahoo.dj yahoo.dk yahoo.dm yahoo.ee yahoo.es yahoo.fi yahoo.fm yahoo.fr yahoo.ge yahoo.gg yahoo.gl yahoo.gm yahoo.gp yahoo.gr yahoo.gy yahoo.hk yahoo.hr yahoo.hu yahoo.ie yahoo.im yahoo.in yahoo.is yahoo.it yahoo.je yahoo.jo yahoo.la yahoo.lt yahoo.lu yahoo.lv yahoo.md yahoo.me yahoo.mk yahoo.mw yahoo.mx  yahoo.net yahoo.nl yahoo.no yahoo.nu yahoo.ph yahoo.pl yahoo.pn yahoo.ps yahoo.pt yahoo.ro yahoo.ru yahoo.rw yahoo.se yahoo.sg yahoo.sh yahoo.si yahoo.sk yahoo.sm yahoo.sn yahoo.so yahoo.sr yahoo.st yahoo.tg yahoo.tk yahoo.tl yahoo.tm yahoo.tn yahoo.vg yahoo.ws maktoob.com -myguide.hkyahoo-news.com.hk yahooapis.com yahoodns.netyahoofinance.comyahoohealth.comyahoomusic.comyahoosportsbook.com yho.com yimg.com  ymail.comxn--d1acpjx3f.xn--p1ai  yandex.az  yandex.by yandex.co.il -yandex.com yandex.com.am yandex.com.ge yandex.com.tr  yandex.ee  yandex.kg  yandex.kz  yandex.lt  yandex.lv  yandex.md -yandex.net  yandex.ru  yandex.st  yandex.sx  yandex.tj  yandex.tm  yandex.ua  yandex.uzyandexcloud.net yastatic.net -yandex - -CATEGORY-VPNSERVICES 928.plus -zind.cloud aaex.uk amytele.com -ark.tobaicaonetwork.com bighead.groupblinkload.zone boslife.biz boslife.net  cloudn.me  conair.me cordcloud.org ctyun.online -cylink.procylink0122.icu dlercloud.com dlercloud.org dlercloud.me dleris.best duyaoss.com duyaossr.com hitun.ioxn--90wwvt03e.comjustmysocks.netjustmysocks1.netjustmysocks2.netjustmysocks4.net  maying.coxn--i2ru8q2qg.commocloudplus.com  mray.club muncloud.dog n3ro.fun  n3ro.live n3ro.net nexitally.comnexitallysafe.com nexitcore.com paofu.cloud paofuyun.me renzhe.cloud rixcloud.comshadowsocks.comshadowsocks.nlsquirrelvpn.comsquirrelgroup.net -ssplive.pwchina-internet-exchange.com cxkcloud.com ssrcloud.com ssrcloud.org -ssrpass.pwstc-server.com surflite.co surflite.net vilavpn.com vilavpn.xyz vilavpn1.xyz vilavpn2.xyz vilavpn3.xyz vilavpn4.xyz vilavpn5.xyz vilavpn6.xyz vilavpn7.xyz -4 -ZB -zb.app -zb.com zb.io zb.live -9 -YOUZAN -youzan.com yzcdn.cn youzanyun.com -& -CHINASO chinaso.com -pg0.cn -* -PAYPALaccount-paypal.infoaccount-paypal.netaccount-paypal.orgaccountpaypal.comaccountpaypal.netaccountpaypal.org anfutong.cn -cn anfutong.comanfutong.com.cn -cn beibao.cn -cn -beibao.com beibao.com.cn -cn bill-safe.combillmelater.combillmelater.infobillmelater.net bml.infobuyfast-paysmart.net cashify.comdevtools-paypal.comexperiencebillmelater.comfundpaypal.com -gmoney.org -i-o-u.infologin-paypal.comlogin-paypal.infomywaytopay.infomywaytopay.net -pa9pal.com paaypal.com  paily.net  paily.org -paipal.com -pavpal.com paydiant.com paylike.com -paypa1.com -paypa1.org paypaal.compaypal-activate.compaypal-activate.infopaypal-activate.orgpaypal-apac.compaypal-apps.compaypal-biz.compaypal-brandcentral.compaypal-business.compaypal-business.netpaypal-business.orgpaypal-cardcash.compaypal-cash.compaypal-center.compaypal-center.infopaypal-center.netpaypal-center.orgpaypal-communication.compaypal-communications.compaypal-communications.netpaypal-community.compaypal-community.netpaypal-comunidad.compaypal-corp.compaypal-database.compaypal-database.uspaypal-donations.compaypal-dynamic.compaypal-engineering.compaypal-europe.compaypal-excelinvoicing.compaypal-exchanges.compaypal-forward.compaypal-galactic.compaypal-gift.compaypal-gifts.compaypal-gpplus.compaypal-here.compaypal-hrsystem.compaypal-innovationlab.compaypal-integration.compaypal-japan.compaypal-knowledge.compaypal-labs.compaypal-latam.compaypal-learning.compaypal-login.compaypal-login.infopaypal-login.orgpaypal-login.uspaypal-luxury.compaypal-mainstreet.netpaypal-marketing.compaypal-media.compaypal-merchantloyalty.compaypal-mobilemoney.compaypal-network.orgpaypal-notice.compaypal-notify.compaypal-online.infopaypal-online.netpaypal-online.orgpaypal-optimizer.compaypal-pages.compaypal-photocard.compaypal-plaza.compaypal-portal.compaypal-prepagata.compaypal-prepagata.netpaypal-prepaid.compaypal-profile.compaypal-proserv.compaypal-qrshopping.orgpaypal-recargacelular.compaypal-redeem.compaypal-referral.compaypal-retail.compaypal-scoop.compaypal-search.compaypal-secure.netpaypal-secure.orgpaypal-security.netpaypal-security.orgpaypal-service.orgpaypal-signin.compaypal-signin.uspaypal-special.compaypal-specialoffers.compaypal-sptam.compaypal-support.compaypal-survey.compaypal-survey.orgpaypal-status.compaypal-team.compaypal-viewpoints.net  paypal.ca -paypal.com paypal.com.cn -cn paypal.com.hk paypal.com.sg  paypal.hk paypal.info  paypal.jp  paypal.me paypal.net.cn -cn paypal.org.cn -cn  paypal.so  paypal.uspaypalbeacon.compaypalbenefits.compaypalbrasil.compaypalcommunity.cn -cnpaypalcommunity.compaypalcommunity.netpaypalcommunity.orgpaypalcorp.compaypalcredit.compaypalcreditcard.compaypalgivingfund.org paypalhere.cn -cnpaypalhere.compaypalhere.com.cn -cnpaypalhere.infopaypalhere.netpaypalhere.org paypalhere.tv paypali.net paypalinc.compaypalindia.compaypalinsuranceservices.org paypall.compaypallabs.com paypalme.com paypalnet.net paypalnet.orgpaypalnetwork.infopaypalnetwork.netpaypalnetwork.orgpaypalobjects.compaypalonline.netpaypalonline.orgpaypalprepagata.compaypalprepagata.netpaypalservice.compaypalshopping.compaypalshopping.netpaypalsurvey.com paypalx.com paypaly.com payppal.com payypal.compdncommunity.com -pp-soc.com ppaypal.com  pppds.com pypl.com  pypl.info pypl.net pypl.tvsecure-paypal.infosecurepaypal.info sslpaypal.orgthepaypalshop.comtheshoppingexpresslane.netwww-paypal.info www-paypal.usxn--bnq297cix3a.cn -cnbraintreegateway.combraintreegateway.tvbraintreepayments.combraintreepayments.infobraintreepayments.orgbraintreepayments.tv!braintreepaymentsolutions.combraintreeps.combriantreepayments.netbriantreepayments.tv card.iogetbraintree.com krakenjs.comloanbuilder.comswiftcapital.com simility.com cashify.netswiftbank.info swiftbank.usswiftfinancial.comswiftfinancial.infoswiftfinancial.netvenmo-touch.com  venmo.com -venmo.info  venmo.net  venmo.org  cash2.comfilipino-music.net -s-xoom.comwebmoneyinfo.com2.wiremoneytoirelandwithxoomeasierandcheaper.com wwwxoom.comxoom-experience.com xoom.com xoom.io xoom.net.cn -cn xoom.us xoomcom.comvenmo.s3.amazonaws.com -& -SMZDM  smzdm.com -zdmimg.com - - -CHOCOLATEYchocolatey.org -, -MEDIUM -medium.commedium.systems -" - RASPBERRYPIraspberrypi.org -^ -BITAUTO bitauto.combitautoimg.com cheyisou.com  yiche.com  yccdn.com -. -NOTION  notion.sonotion-static.com -: -WEIPHONE feng.com fengimg.com wfdata.club -! -WANFANGwanfangdata.com.cn - -SHIELDS -shields.io - -CCTV cctv.cn cctv.com cctvpic.com chinaepg.cn chinalive.com citv.net.cn cnms.net.cn cntv.cn cntv.com.cn -cctvlib.cncctvlibrary.cncctvlib.com.cncctvlibrary.com.cn -cctvpro.cncctvpro.com.cn  cntvwb.cn gjgbdszt.cngjgbdszt.com.cngjgbdszt.net.cn  ipanda.cn -ipanda.com ipanda.com.cn -ipanda.net livechina.cn livechina.comolympicchannelchina.cn tvcc.cn tvcc.com.cn%!xn--fiq53l6wcx3kp9bc7joo6apn8a.cn%!xn--fiq53l90et9fpncc7joo6apn8a.cn-)xn--fiq53l6wcx3kp9bc7joo6apn8a.xn--fiqs8s%!xn--kprv4ewxfr9cpxcc7joo6apn8a.cn-)xn--kprv4ewxfr9cpxcc7joo6apn8a.xn--fiqs8s zggbdszt.cnzggbdszt.com.cnzggbdszt.net.cnzygbdszt.net.cn -k - CHEETAHMOBILE cmcm.com cmcmcdn.com  liebao.cn duba.com duba.net ijinshan.com - -MINGW  mingw.org - -IUS -ius.io - -OOKLA-SPEEDTEST  cdnst.net cellmaps.com ekahau.cloud -ekahau.com  ookla.comooklaserver.net pingtest.net speedtest.co speedtest.netspeedtestcustom.com webtest.netOB^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$ -ads - - -CRAIGSLISTcraigslist.org -5 -V2RAY  v2ray.com -v2ray.cool  v2fly.org -m -WPS -wps.cn  51dzt.com  iciba.com  docer.com kdocs.cn papocket.com  wpspdf.cn - -TGBUS  tgbus.com -o -UNITY  unity.com unity3d.com!unityads.unity3d.com -ads#unityads.unitychina.cn -ads -S - UNITY-ADS!unityads.unity3d.com -ads#unityads.unitychina.cn -ads -+ -BAIDU a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -ads zuoyebang.cc zuoyebang.com -zybang.com baidu -91.com -aipage.com apollo.auto baidu.cn  baidu.com baidu.com.cn baidubce.combaiducontent.com baidupcs.combaidustatic.com -baifae.com baifubao.com -bcebos.com  bdimg.com bdstatic.com bdtjrcv.com  bdydns.cn -bdydns.com -bdydns.net chuanke.com  dlnel.com  dlnel.org -duapps.com -dwz.cn -hao123.com hao123img.com -hao222.com -haokan.com jomocdn.net jomodns.com -mipcdn.com  nuomi.com quyaoya.com smartapps.cn  tieba.com tiebaimg.com xianfae.com xiaodutv.com - - TRUSTWAVE trustwave.com - -VK mvk.com userapi.com  vk-cdn.me vk-portal.net vk.cc -vk.com  vk.design vk.link vk.me vkcache.com vkgo.app -vklive.appvkmessenger.comvkmessenger.app vkontakte.ruvkuseraudio.comvkuserlive.netvkuservideo.comvkuservideo.net -; -XUEERSI speiyou.com -xesimg.com xueersi.com - -DETTOLdettol-prize.com  dettol.at  dettol.be  dettol.ch  dettol.cl dettol.co.id dettol.co.in dettol.co.ke dettol.co.nz dettol.co.uk -dettol.com dettol.com.au dettol.com.bd dettol.com.br dettol.com.cn -cn dettol.com.eg dettol.com.hk dettol.com.my dettol.com.ng dettol.com.sg  dettol.cz  dettol.fr  dettol.hu  dettol.ie -dettol.net  dettol.nl  dettol.pk  dettol.pl  dettol.pt  dettol.rudettolarabia.comdettolcleannaija.comdettolsitishield.co.indettolthailand.comespadoldettol.com.ar - -GENTOO -gentoo.org -- -LANTERNgetlantern.org -lantern.io -o -IFANR coolbuy.com  ifanr.com ifanr.inifanrusercontent.com -ifanrx.com -minapp.com -" - SINOINSIDERsinoinsider.com - -WTO wto.org - -JUEJIN  juejin.imb-gold-cdn.xitu.iomirror-gold-cdn.xitu.iop1-juejin.byteimg.comp2-juejin.byteimg.comp3-juejin.byteimg.comp4-juejin.byteimg.comp5-juejin.byteimg.comp6-juejin.byteimg.comp7-juejin.byteimg.comp8-juejin.byteimg.comp9-juejin.byteimg.com - -MUJI muji.com muji.com.cn -cn muji.com.hk muji.eu muji.net muji.tw muji.us mujikorea.net -B -RBaerogard.com.auaerogard.com.cn -cnaerogardcn.com -airwick.at -airwick.be -airwick.ca -airwick.ch -airwick.cl airwick.co.in airwick.co.nz airwick.co.uk airwick.co.za airwick.comairwick.com.auairwick.com.brairwick.com.cn -cnairwick.com.mxairwick.com.tr -airwick.cz -airwick.de -airwick.dk -airwick.es -airwick.fi -airwick.fr -airwick.hr -airwick.hu -airwick.ie -airwick.it -airwick.jp airwick.net -airwick.nl -airwick.no -airwick.pl -airwick.pt -airwick.ro -airwick.ru -airwick.se -airwick.si -airwick.sk -airwick.tv -airwick.usairwickarabia.com vipoo.es  calgon.at  calgon.be  calgon.ch calgon.cn -cn calgon.co.uk -calgon.com calgon.com.tr  calgon.de  calgon.es  calgon.fr  calgon.ie  calgon.it  calgon.nl  calgon.pt  calgon.ru  calgon.tvcalgoncarbon-china.comcalgoncarbon.com calgonit.com clearasil.cn -cnclearasil.com.cn -cn clearasil.usdettol-prize.com  dettol.at  dettol.be  dettol.ch  dettol.cl dettol.co.id dettol.co.in dettol.co.ke dettol.co.nz dettol.co.uk -dettol.com dettol.com.au dettol.com.bd dettol.com.br dettol.com.cn -cn dettol.com.eg dettol.com.hk dettol.com.my dettol.com.ng dettol.com.sg  dettol.cz  dettol.fr  dettol.hu  dettol.ie -dettol.net  dettol.nl  dettol.pk  dettol.pl  dettol.pt  dettol.rudettolarabia.comdettolcleannaija.comdettolsitishield.co.indettolthailand.comespadoldettol.com.ardewitwithdurex.com dotherex.com durex-shop.chdurex-shopline.comdurex-slovenia.si durex.at durex.be durex.cl durex.co.id durex.co.il durex.co.nz durex.co.th durex.co.uk durex.co.za  durex.com durex.com.au durex.com.bd durex.com.cn -cn durex.com.co durex.com.hr durex.com.my durex.com.ng durex.com.pe durex.com.ph durex.com.pk durex.com.sg durex.com.tr durex.cz durex.de durex.dk durex.ee durex.es durex.fi durex.fr durex.hu durex.ie durex.it durex.jp durex.lv durex.mx durex.nl durex.no durex.pl durex.pt durex.ro durex.ru durex.se durex.us durexcam.comdurexcanada.comdurexchina.comdurexchina.com.cn -cndurexindia.comdurexloveclub.comdurexukraine.com durexusa.comlovesexdurex.cn -cnlovesexdurex.complaybydurex.com shopdurex.com trydurex.net trydurex.tvbebepremium3.com.bo -enfa.co.id enfa.com.vn enfaaplus.com enfababy.com enfabebe.comenfabebe.com.brenfabebe.com.coenfabebe.com.doenfabebe.com.ecenfabebe.com.mxenfabebe.com.peenfabebe.com.veenfabebe3.com.arenfagrow.co.inenfagrow.com.bnenfagrow.com.myenfagrow.com.phenfagrow.com.sg enfagrow4.comenfamama.com.arenfamama.com.ph -enfamil.ca enfamil.com -enfamil.es -enfamil.pl -enfamil.pt enfasmart.comenspireformula.comnutramigen.net nutramigen.pl$dishwashingexpert.com.cn -cn  finish.at  finish.bg finish.co.kr finish.co.nz finish.co.uk finish.co.za finish.com.cn -cn finish.com.hr finish.com.tr  finish.de  finish.es  finish.fr  finish.gr  finish.hu  finish.lv  finish.pl  finish.pt  finish.ro  finish.si  finish.skfinisharabia.comfinishbrasil.com.brfinishdishwashing.cafinishdishwashing.com finishinfo.be finishinfo.clfinishinfo.comfinishinfo.com.arfinishinfo.com.au finishinfo.cz finishinfo.fi finishinfo.it finishinfo.jp finishinfo.nl finishinfo.no finishinfo.ru finishinfo.sefinishkilpailu.fi finishwin.beyarininsuyu.com lizol.co.in lysol.ca lysol.cllysol.cn -cn lysol.co.cr  lysol.com lysol.com.cn -cn lysol.com.mx  lysol.netenfinitas.com.cn -cnmeadjohnson.commeadjohnson.com.cn -cnmeadjohnson.com.hkmeadjohnson.com.twmeadjohnson.net -mortein.cn -cn mortein.co.in mortein.co.nz mortein.co.za mortein.commortein.com.aumortein.com.brmortein.com.ngmortein.com.pkmorteincam.compifpafarabia.comkeepmovingwithmovefree.com move-free.net movefree.cn -cn movefrees.commovefreeoffers.commovefreerewards.comschiffvitamins.com -mucinex.ca -mucinex.cn mucinex.co.nz mucinex.commucinex.com.cn mucinex.net -mucinex.tvmucinexprofessional.com -nurofen.at -nurofen.bg -nurofen.cn -cn nurofen.co.il nurofen.co.nz nurofen.co.uk nurofen.co.za nurofen.comnurofen.com.aunurofen.com.cn -cnnurofen.com.sg -nurofen.cz -nurofen.de -nurofen.es -nurofen.fr -nurofen.hr -nurofen.hu -nurofen.ie -nurofen.it nurofen.net -nurofen.no -nurofen.pl -nurofen.pt -nurofen.ro -nurofen.sk -nurofen.runurofengel.com51nurofensk-prod-env.eu-west-1.elasticbeanstalk.com -cepacol.ca cepacol.com dobendan.degraneodin.com.mx -repsils.no strepsils.at strepsils.ch strepsils.cn -cnstrepsils.co.krstrepsils.co.nzstrepsils.co.zastrepsils.com.arstrepsils.com.brstrepsils.com.cn -cnstrepsils.com.costrepsils.com.austrepsils.com.hkstrepsils.com.phstrepsils.com.tw strepsils.es strepsils.fi strepsils.fr strepsils.hr strepsils.hu strepsils.ie strepsils.net strepsils.pl strepsils.pt strepsils.ro strepsils.ru strepsils.si strepsils.usstrepsilsarabia.como2action.co.krpowerofresolve.capowerofresolve.comspraynwash.com  vanish.at  vanish.be  vanish.ch  vanish.cl vanish.cn -cn vanish.co.id vanish.co.il vanish.co.in vanish.co.nz vanish.co.uk vanish.co.za vanish.com.ar vanish.com.br vanish.com.co vanish.com.hr vanish.com.mx vanish.com.my vanish.com.pe vanish.com.sg vanish.com.tr  vanish.de  vanish.dk  vanish.es  vanish.fi  vanish.fr  vanish.hu  vanish.ie  vanish.it  vanish.nl  vanish.no  vanish.pl  vanish.pt  vanish.ro  vanish.ru  vanish.se  vanish.si  vanish.skvanisharabia.comvanishbancaseulook.com.brvanishcentroamerica.com vanishinfo.czvanishstains.comvanishstains.com.au veet.at veet.be veet.ca veet.ch veet.clveet.cn -cn -veet.co.id -veet.co.in -veet.co.kr -veet.co.nz -veet.co.uk -veet.co.za veet.com veet.com.ar veet.com.au veet.com.bd veet.com.br veet.com.cn -cn veet.com.co veet.com.hk veet.com.mx veet.com.my veet.com.ph veet.com.pk veet.com.sg veet.com.tr veet.de veet.dk veet.es veet.fi veet.fr veet.hu veet.jp veet.nl veet.no veet.pt veet.ro veet.ru veet.se veet.tv veet.usveetarabia.comveetbangladesh.comveetcentroamerica.com veetclub.it -woolite.ca -woolite.cn -cn woolite.comwoolite.com.cn -cn -woolite.pl -woolite.uswoolitecarpet.comdigital-rb.com rb-crisis.com -rb.com -rb.netrbbusinessshop.comrbdigitalab.comrbdigitallab.comrbeuroinfo.com rbgrads.comrbgraduates.comrbmavericks.comrbnainternational.com  rbplc.comrbrandlibrary.com rbspeakup.com reckitt.netreckittbenckiser.cn -cnreckittbenckiser.com#reckittbenckiser.com.cn -cnreckittbenckiser.netreckittbenckiser.tvreckittprofessional.com - -GUOKR  guokr.com - -MARIADB mariadb.org -h -51JOB  51job.com 51jobcdn.comyingjiesheng.comyingjiesheng.netzhiding.com.cn - -AKASHA akasha.world - -CAIXIN -caixin.com - - -FANHAODIANfanhaodian.com - -JD-ADSads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads - - LINUXMINT linuxmint.com - - -STATICFILEstaticfile.org - - BLINKLOADblinkload.zone - -CATEGORY-ENTERTAINMENT abema.io abema.tv ameba.jp ao3.orgarchiveofourown.orgarchiveofourown.comarchiveofourown.net -att.tv atttvnow.comattwatchtv.com directv.com  dtvce.comnettyinternet.com gamer.com.twbahamut.com.twgamer-cds.cdn.hinet.netgamer2-cds.cdn.hinet.net bandcamp.com -bcbits.com bethesda.net doom.comelderscrolls.com blizzard.cn -cn blizzard.comblizzardgearstore.comblz-contentstack.com blzstatic.cn -cn -battle.netbattlenet.com.cn -cn bnet.163.com -cnblizzard.nefficient.co.krblzddist1-a.akamaihd.netblzddistkr1-a.akamaihd.netbnetcmsus-a.akamaihd.netbnetproduct-a.akamaihd.netbnetshopus.akamaized.net diablo3.comdiabloimmortal.comdi.res.netease.com -cn!diablo3.nosdn.127.net -cnfiresidegatherings.complayhearthstone.com%hearthstone.nosdn.127.net -cnheroesofthestorm.com%hearthstone.nosdn.127.net -cn"heroes.nos.netease.com -cnplayoverwatch.com#overwatch.nosdn.127.net -cn starcraft.comstarcraft2.comsc2.nosdn.127.net -cnplaywarcraft3.comworldofwarcraft.com wowchina.com -cnwow.nosdn.127.net -cnblizzcon-a.akamaihd.netblz.nosdn.127.net -cnblzmedia-a.akamaihd.netblznav.akamaized.net blubrry.com  castro.fmdeviantart.comdeviantart.net da-files.comabc-studios.com abc.com edgedatg.com espn.com espncdn.com watchespn.comespn.hb.omtrdc.netespndotcom.tt.omtrdc.net -marvel.commarvel10thanniversary.commarveldimensionofheroes.commarvelparty.netmarvelpinball.commarvelsdoubleagent.commarvelspotlightplays.com marvelsuperheroseptember.commarvelsuperwar.comthemarvelexperiencetour.comnatgeomaps.comnationalgeographic.com"nationalgeographicpartners.com ngeo.comadventuresbydisney.com -babble.com -dilcdn.comdisney-asia.comdisney-discount.comdisney-plus.netdisney-studio.comdisney-studio.net  disney.ca -disney.com disney.com.br  disney.iodisneyadsales.comdisneyarena.comdisneyaulani.comdisneybaby.comdisneycareers.comdisneychannelonstage.comdisneychannelroadtrip.comdisneycruisebrasil.comdisneyenconcert.comdisneyiejobs.comdisneyinflight.comdisneyjunior.comdisneyjuniortreataday.comdisneylatino.comdisneymeetingsandevents.comdisneymovieinsiders.comdisneymusicpromotion.comdisneynewseries.com disneynow.comdisneypeoplesurveys.comdisneyplus.comdisneyredirects.comdisneysrivieraresort.comdisneystore.comdisneysubscription.comdisneytvajobs.comdisneyworld-go.com -dssott.comgo-disneyworldgo.com -go.com  mickey.tvmoviesanywhere.complaymation.comshopdisney.comshops-disney.comsorcerersarena.comspaindisney.com starwars.com#starwarsgalacticstarcruiser.comstreamingdisney.netthestationbymaker.comthisispolaris.comwatchdisneyfe.com dawngate.com eastore.comlordofultima.com -origin.com  origin.tv#cloudsync-prod.s3.amazonaws.comorigin-a.akamaihd.netrtm.tnt-ea.comssl-lvlt.cdn.ea.comawayoutgame.combattlefieldbadcompany2.combattlefront2.combattlefrontii.combejewled-stars.combejeweledstars.comcapitalgames.com chillingo.comcommandandconquer.comconquerwithcharacter.com  crysis.jp dawngate.comdawngatechronicles.com -dicela.com -dicela.netea-anz-press.com -ea.com eaaccess.comeacashcard.com eacodigos.com eakorea.co.kr eamobile.com eanordic.com -eaplay.com easports.com easports.jpeasportsactive.comeasportsactiveonline.comeasportsfootball.comeasportsfootballclub.comeasportsmma.comeasportsworld.comelectronicarts.comelectronicarts.fr futpromos.com futunited.comghostgames.comheroesofdragonage.com hutpromos.commaddenchampionship.commaddenrewards.commaddenseason.infomaddenseason.netmaddenseason.org  maxis.commysimsracing.com play4free.com playapex.com pogo.com pogobeta.com -popcap.comprojectapex.com pvzheroes.com spearhead.krspeedhunters.com  spore.com ssx3.comstarwarsbattlefront.comstarwarsbattlefront2.comtiberiumalliances.com tiburon.com -tnt-ea.comultimaforever.com unravel2.comunraveltwo.comvisceralgames.com -yogify.comanthemgame.comanthemthegame.comapexlegends.comprojectapex.combattlefield.combattlefield1943.combattlefield3.combattlefield4.combattlefield5.combattlefieldheroes.combattlefieldv.com battlelog.com bioware.combiowarestore.com eamythic.com eamythic.net cncrivals.comcommandandconquer.comtiberiumalliances.comcamelot-europe.comcamelotherald.comcamelotherald.net daoc.netdarkageofcamelot.comdarkness-risen.comdeadspacegame.com dice.se dragonage.comdragonagekeep.comdragonagemovie.comthedreadwolfrises.comdungeonkeeper.cndungeonkeeper.comdungeonkeeper.com.cneafootballworld.comfifastreet.comfifastreet3.com frostbite.comindustrialtoys.commasseffect.commasseffectarchives.comeamirrorsedge.commirrorsedge.commirrorsedge.jpmirrorsedge.netmirrorsedge2.commirrorsedge2d.comblackboxgames.comeablackbox.comneedforspeed.comneedforspeedboost.comneedforspeeddriftkings.comneedforspeedeliminator.comneedforspeedlightning.comneedforspeedoverdrive.comneedforspeedproven.comneedforspeedredline.comneedforspeedshowdown.comneedforspeedstreetkings.comneedforspeedtakedown.comneedforspeedtherun.comneedforspeedtimeattack.com#needforspeedundergroundeast.com nfsworld.comteamneedforspeed.complantsvszombies2.com -pvzgw2.com pvzheroes.comseaofsolitude.comsimcity-buildit.com simcity.com -skate2.com spearhead.krstarwarsfallenorder.comstarwarsjedifallenorder.comstarwarstheoldrepublic.comswjedifallenorder.com  swjfo.com  swtor.com  swtor.net -mysims.com thesims.com thesims3.com thesims4.comthesimssocial.com titanfall.commythicentertainment.commythicentertainment.netmythicgames.comultimaonline.com -uo.com uoherald.com xdsummit.comea.tt.omtrdc.neteaassets-a.akamaihd.netbattlebreakers.com epicgames.com fortnite.com paragon.complayparagon.comroborecall.comshadowcomplex.com spyjinx.comunrealengine.comunrealtournament.com6*^cdn\d-epicgames-\d+\.file\.myqcloud\.com$ -cn*&^epicgames-download\d\.akamaized\.net$;/^epicgames-download\d-\d+\.file\.myqcloud\.com$ -cn fireside.fm gog.comgog-statics.comgog.qtlglb.com -cn$gogalaxy.gog-statics.com -cn'menu-static.gog-statics.com -cn'productcard.gog-statics.com -cn(static-login.gog-statics.com -cn'www4-static.gog-statics.com -cn, ^images(-\d)?\.gog-statics\.com$ -cn cinemax.comforthethrone.com hbo.com hboasia.com  hbogo.com hbogoasia.com hbogoasia.hk -hbomax.com -hbonow.com  maxgo.com -112263.com callhulu.comfindyourlimits.com freehulu.com  hooloo.tv  hoolu.com hoolu.tv hu1u.com huloo.cc huloo.tv hulu.com hulu.tv hulu.ushuluaction.com -huluad.com huluapp.com huluasks.com hulucall.com hulufree.com hulugans.comhulugermany.com -hulugo.com -huluim.comhuluinstantmessenger.com huluitaly.com hulunet.comhulunetwork.com huluplus.comhulupremium.comhulupurchase.com -huluqa.comhulurussia.com huluspain.comhulusports.comhulustream.com huluteam.com -hulutv.com huluusa.comjoinmaidez.com mushymush.tv -myhulu.comoriginalhulu.com payhulu.comregisterhulu.comthehulubraintrust.comwwwhuluplus.com imdb.com imdb.tomedia-imdb.cominternationalsaimoe.comkingkong.com.twkingkongapp.com litv.tv megaphone.fm moov.hk fast.com -netflix.ca netflix.com netflix.netnetflixinvestor.comnetflixtechblog.com nflxext.com nflximg.com nflximg.netnflxsearch.net -nflxso.net nflxvideo.netnico -nicodic.jp nicomanga.jp niconico.com nicoseiga.jp nicovideo.jp nimg.jp simg.jp100classicbooks.com -amiibo.comayudanintendo.combowsersinsidestory.comexcitebots.comflipnotestudio.comgloryofheracles.comhackyourconsole.comkirbysuperstarultra.comkirbysepicyarn.comlegendofzelda.cn -cn legendofzelda.com.cn -cn miitomo.commiitomo.com.cn -cn miiverse.com nentindo.netnintendo-europe-sales.comnintendo-europe.com nintendo.at nintendo.be nintendo.chnintendo.co.jpnintendo.co.krnintendo.co.uknintendo.co.za nintendo.comnintendo.com.hknintendo.com.pt nintendo.de nintendo.dk nintendo.es nintendo.eu nintendo.fi nintendo.fr nintendo.it nintendo.net nintendo.nl nintendo.no nintendo.pt nintendo.ru nintendo.se nintendo.twnintendo3ds.comnintendodsi.comnintendoeurope.comnintendolabo.cn -cnnintendonetwork.netnintendonyc.comnintendostore.comnintendoswitch.comnintendoswitch.netnintendoswitchtogether.compersonaltrainermath.complaynintendo.comsupersmashbros.cn -cn!supersmashbros.com.cn -cnthelegendarystarfy.comthewonderful101.comwariolandshakeit.comwariowarediy.comyoshisnewisland.comac-pocketcamp.comdiddykongracing.com dkr.com donkeykongcountryreturns.comfire-emblem-heroes.comfireemblemawakening.com -dialga.com giratina.com -kyurem.com pokedex3d.compokemon-moon.compokemon-sun.compokemon-sunmoon.com pokemon.compokemonbank.compokemonblackwhite.com pokemonbw.compokemonchampionships.compokemongoldsilver.compokemonhome.compokemonletsgoeevee.compokemonletsgopikachu.compokemonmysterydungeon.compokemonpicross.compokemonplatinum.compokemonrubysapphire.compokemonsunmoon.compokemonswordshield.compokemonultrasunmoon.compokemonvgc.compokemonwifi.net xn--mts47c3w9b1qr.cn -cnxn--mts47c3w9b1qr.netsplatoon2tournament.com starfox.comdrmario-world.commarioandluigidreamteam.commariobroswii.com mariokart.cn -cnmariokart.com.cn -cnmariokart7.commariokart8.commariosupersluggers.comnewsupermariobrosu.comsupermario.comsupermario3dworld.com!supermariobros.com.cn -cnsupermariogalaxy.comsupermariorun.comsuperpapermario.comnintendowii.com  wii-u.com -wiifit.com wiifitu.com wiipartyu.com wiisports.comwiisportsresort.comwiiugamepad.com  wiivc.net xenoblade.com op.ggopgg-static.akamaized.net overcast.fm  pixiv.net  pixiv.org pixiv.co.jp  pximg.net playstationplaystation.complaystation.net sonyentertainmentnetwork.com -pca.stpocketcasts.comhistoryofdota.comhistoryofdota.nethistoryofdota.orginstituteofwar.orgmolesports.com rgpub.ioriot-games.com riot.com riot.net riotcdn.netriotgames.co.kr riotgames.comriotgames.info riotgames.jp riotgames.net riotgames.tv riotpin.comriotpoints.com rstatic.net supremacy.com supremacy.net championshipseriesleague.com lcsmerch.comleaguehighschool.comleagueoflegends.caleagueoflegends.cn -cnleagueoflegends.co.krleagueoflegends.comleagueoflegends.infoleagueoflegends.krleagueoflegends.netleagueoflegends.orgleagueoflegendsscripts.comleaguesharp.infoleaguoflegends.comlearnwithleague.comlol-europe.com lolclub.org lolespor.comlolesports.com lolfanart.net -lolpcs.com lolshop.co.kr lolstatic.comlolusercontent.com -lpl.com.cn -cn pvp.net -pvp.tv ulol.comlolstatic-a.akamaihd.netrockstargames.com -rsg.sc1-gamedownloads-rockstargames-com.akamaized.net)%media-rockstargames-com.akamaized.net*&videos-rockstargames-com.akamaized.net fandango.com mgo.commgo-images.com ravm.tv roku.com rokutime.com dishworld.comslinginternational.com  sling.commovenetworks.com -movetv.comimageworks.commotionpictureser.com playshow.ioplayshowtv.comsonyclassics.comsonypictures.comsonypicturesanimation.comsonypicturesmuseum.comsonypicturesstudios.comsonypicturestelevision.com#sonypicturestelevisiongames.com wheelpop.com -wwtbam.comyourfantasybeginsnow.comsoundcloud.com -sndcdn.com byspotify.com pscdn.co scdn.co spoti.fispotify-everywhere.com spotify.comspotify.designspotifycdn.comspotifycharts.comspotifycodes.comspotifyforbrands.comspotifyjobs.com fanatical.comhumblebundle.complayartifact.com -s.teamsteam-chat.comsteamcommunity.comsteamcontent.comsteamgames.comsteampowered.comsteamstatic.comsteamusercontent.comunderlords.comvalvesoftware.com wmsjsteam.com -cn csgo.wmsj.cn -cndl.steam.ksyna.com -cn dota2.wmsj.cn -cn st.dl.bscstorage.net -cnst.dl.eccdnx.com -cn!st.dl.pinyuncloud.com -cn&steampowered.com.8686c.com -cn%steamstatic.com.8686c.com -cn steambroadcast.akamaized.netsteamcdn-a.akamaihd.net!steamcommunity-a.akamaihd.netsteamstore-a.akamaihd.net#steamusercontent-a.akamaihd.net"steamuserimages-a.akamaihd.netsteamunlocked.net syosetu.com -muscdn.com -musical.ly -tiktok.com tiktokcdn.com tiktokv.com#p16-tiktokcdn-com.akamaized.net ext-twitch.tv  jtvnw.net  ttvnw.net  twitch.tv twitchcdn.net twitchsvc.net ubi.com ubisoft.com  uplay.com,(ubisoft-uplay-savegames.s3.amazonaws.com,(ubisoft-orbit-savegames.s3.amazonaws.com)uplaypc-s-ubisoft.cdn.ubi.com -cn -vhx.tv -vhxqa1.com -vhxqa2.com -vhxqa3.com -vhxqa4.com -vhxqa6.comvimeo-staging.comvimeo-staging2.com  vimeo.com vimeo.frvimeobusiness.com vimeocdn.comvimeogoods.comvimeoondemand.comvimeostatus.comlivestream.com abow.jp angelbeats.jp -anigema.jpcharlotte-anime.jpclannad-movie.jpheaven-burns-red.comkamisama-day.jpkamisama-maeda-lab.com koubaibu.jplitbus-anime.com product.co.jprewrite-anime.tvvisualarts.gr.jpvisual-arts.jp -viu.tvapi.viu.now.com^ewcdn[0-9]{2}\.nowe\.com$ viu.com dfp6rglgjqszk.cloudfront.net!d1k2us671qcoau.cloudfront.net!d2anahhhmp1ffz.cloudfront.net -weverse.io wwe.com wwe9.comcallersbane.com minecraft.netminecraftshop.com -mojang.comforzamotorsport.netforzaracingchampionship.com forzarc.comorithegame.comrenovacionxboxlive.comtellmewhygame.com xbox.co xbox.com xbox.eu xbox.org -xbox360.co xbox360.com -xbox360.eu xbox360.org -xboxab.comxboxgamepass.comxboxgamestudios.com xboxlive.cn -cn xboxlive.com -xboxone.co xboxone.com -xboxone.euxboxplayanywhere.comxboxstudios.com yande.re -got001.com -got002.com jstucdn.com playcvn.com rrys.tv rrys2019.com rrysapp.com  yyets.com zmz2019.com -zmzapi.com cvnad.com -ads -iyoowi.com -ads -Q - CHINANEWS chinanews.comchinanews.com.cn -cns.com.cn ecns.cn -$ -CAS -csdl.ac.cn  las.ac.cn - - NBCUNIVERSALdreamworks.com cnbc.com -cnbcfm.com  msnbc.com -nbc.co nbc.com nbcnews.comnbcolympics.com nbcsports.com -nbcuni.comnbcuniversal.com s-nbcnews.com peacocktv.comuniversalorlando.comuniversalpictures.com&"universalpicturesinternational.com!universalstudioshollywood.com - -115 115.com - - W3SCHOOLS w3schools.com -c -YOUKU -kumiao.com  youku.com  ykimg.com -mmstat.com soku.com -cibntv.net -) -LIANJIA lianjia.com  ljcdn.com -( -OPENCOLLECTIVEopencollective.com -: -POLYMERpolymer-project.orgpolymerproject.org - - CHINATELECOM -ctexcel.ca ctexcel.comctexcel.com.hk -ctexcel.fr -ctexcel.us -189.cnchinatelecom-h.comchinatelecom.com.cnchinatelecomglobal.com -chntel.com - -EPORNER eporner.com -2 -KU6 ku6.com -ku6cdn.com -ku6img.com - -TIPDM -tipdim.org -. -FLUTTER flutter.devflutterapp.com -, -LINUXFROMSCRATCHlinuxfromscratch.org -+ - SEGMENT-ADScdn.segment.com -ads - - -NODESOURCEnodesource.com - -OPENSUSE opensuse.org -A -ACERacer-group.com acer.com acer.com.cn -cn - -FEEDLY -feedly.com - -KERNEL -kernel.org -$ -TAOMEE -61.com -taomee.com - - -VISUALARTS abow.jp angelbeats.jp -anigema.jpcharlotte-anime.jpclannad-movie.jpheaven-burns-red.comkamisama-day.jpkamisama-maeda-lab.com koubaibu.jplitbus-anime.com product.co.jprewrite-anime.tvvisualarts.gr.jpvisual-arts.jp -F - IDENTRUST identrust.comidentrust.co.uk identrust.net -0 -OPENWRT openwrt.orglede-project.org - -SOHU-ADSadnet.sohu.com -ads ads.sohu.com -ads!adv-sv-show.focus.cn -ads aty.sohu.com -adsdata.vod.itc.cn -adsepro.sogou.com -ads go.sohu.com -adsgolden1.sogou.com -adsimp.optaim.com -adsinte.sogou.com -adsinte.sogoucdn.com -adslu.sogoucdn.com -ads pv.sohu.com -adstheta.sogoucdn.com -adstrack.sohu.com -adsuranus.sogou.com -adsv2.reachmax.cn -ads)e8aeb8bbdbbd7.cdn.sohucs.com -ads)643108e7617ef.cdn.sohucs.com -ads -* -TELEKOM telekom.com -telekom.de -, -THENATIONALPULSEthenationalpulse.com -c - UMENG-ADSalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads -, -BANDCAMP bandcamp.com -bcbits.com - -FFMPEG -ffmpeg.org -$ -SCMP scmp.com -i-scmp.com -9 -ABCabc-studios.com abc.com edgedatg.com - -CUP -cup.com.hk - -VERIZON -flickr -flickr.com -flickr.netstaticflickr.com ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -ads yahoo yahoo.ae yahoo.am yahoo.as yahoo.at yahoo.az yahoo.ba yahoo.be yahoo.bg yahoo.bi yahoo.bs yahoo.bt yahoo.by yahoo.ca  yahoo.cat yahoo.cd yahoo.cg yahoo.ch yahoo.cl yahoo.cmyahoo.cn -cn yahoo.co.ao yahoo.co.bw yahoo.co.ck yahoo.co.cr yahoo.co.id yahoo.co.il yahoo.co.in yahoo.co.jp yahoo.co.kr yahoo.co.mz yahoo.co.nz yahoo.co.th yahoo.co.tz yahoo.co.uk yahoo.co.uz yahoo.co.ve yahoo.co.vi yahoo.co.za  yahoo.com yahoo.com.af yahoo.com.ag yahoo.com.ai yahoo.com.ar yahoo.com.au yahoo.com.bd yahoo.com.bo yahoo.com.br yahoo.com.bz yahoo.com.cn -cn yahoo.com.co yahoo.com.do yahoo.com.ec yahoo.com.eg yahoo.com.es yahoo.com.fj yahoo.com.gi yahoo.com.gt yahoo.com.hk yahoo.com.kw yahoo.com.lb yahoo.com.ly yahoo.com.mt yahoo.com.mx yahoo.com.my yahoo.com.na yahoo.com.nf yahoo.com.om yahoo.com.pa yahoo.com.pe yahoo.com.ph yahoo.com.pk yahoo.com.pr yahoo.com.py yahoo.com.sa yahoo.com.sb yahoo.com.sg yahoo.com.sv yahoo.com.tj yahoo.com.tr yahoo.com.tw yahoo.com.ua yahoo.com.uy yahoo.com.vc yahoo.com.vn yahoo.cz yahoo.de yahoo.dj yahoo.dk yahoo.dm yahoo.ee yahoo.es yahoo.fi yahoo.fm yahoo.fr yahoo.ge yahoo.gg yahoo.gl yahoo.gm yahoo.gp yahoo.gr yahoo.gy yahoo.hk yahoo.hr yahoo.hu yahoo.ie yahoo.im yahoo.in yahoo.is yahoo.it yahoo.je yahoo.jo yahoo.la yahoo.lt yahoo.lu yahoo.lv yahoo.md yahoo.me yahoo.mk yahoo.mw yahoo.mx  yahoo.net yahoo.nl yahoo.no yahoo.nu yahoo.ph yahoo.pl yahoo.pn yahoo.ps yahoo.pt yahoo.ro yahoo.ru yahoo.rw yahoo.se yahoo.sg yahoo.sh yahoo.si yahoo.sk yahoo.sm yahoo.sn yahoo.so yahoo.sr yahoo.st yahoo.tg yahoo.tk yahoo.tl yahoo.tm yahoo.tn yahoo.vg yahoo.ws maktoob.com -myguide.hkyahoo-news.com.hk yahooapis.com yahoodns.netyahoofinance.comyahoohealth.comyahoomusic.comyahoosportsbook.com yho.com yimg.com  ymail.com -oath.cloud oath.com ouroath.comstatic-verizon.com verizon.com verizon.netverizonbusinessfios.comverizonenterprise.comverizonfios.comverizonmedia.comverizonwireless.com vzw.com -xo.com - -STEAM fanatical.comhumblebundle.complayartifact.com -s.teamsteam-chat.comsteamcommunity.comsteamcontent.comsteamgames.comsteampowered.comsteamstatic.comsteamusercontent.comunderlords.comvalvesoftware.com wmsjsteam.com -cn csgo.wmsj.cn -cndl.steam.ksyna.com -cn dota2.wmsj.cn -cn st.dl.bscstorage.net -cnst.dl.eccdnx.com -cn!st.dl.pinyuncloud.com -cn&steampowered.com.8686c.com -cn%steamstatic.com.8686c.com -cn steambroadcast.akamaized.netsteamcdn-a.akamaihd.net!steamcommunity-a.akamaihd.netsteamstore-a.akamaihd.net#steamusercontent-a.akamaihd.net"steamuserimages-a.akamaihd.net - - TEST-IPV6 test-ipv6.com -4 - ARCHLINUX archlinux.orgarchlinuxarm.org - -BITMEX -bitmex.com - -ELSEVIER els-cdn.comelsevier-ae.com elsevier.com elsevier.ioengineeringvillage.com  evise.com -scopus.com - -LINOTYPE linotype.com - -MUNCLOUD muncloud.dog -L - -TEAMBITIONteambition.comteambition.netteambitionapis.com - -UNWIRE  unwire.hk - -AVGLE  avgle.com - - CALGONCARBON  calgon.at  calgon.be  calgon.ch calgon.cn -cn calgon.co.uk -calgon.com calgon.com.tr  calgon.de  calgon.es  calgon.fr  calgon.ie  calgon.it  calgon.nl  calgon.pt  calgon.ru  calgon.tvcalgoncarbon-china.comcalgoncarbon.com calgonit.com - -GCORES -gcores.com -L -LANZOU baidupan.com -lanzou.com lanzous.com woozooo.com - -MRAY  mray.club -/ - STARTPAGE startpage.com ixquick.com - -SUPERSONIC-ADSsupersonic.com -adssupersonicads.com -ads -ssacdn.com -ads)supersonicads-a.akamaihd.net -ads - -UNPKG  unpkg.com - -ADOBE typekit.com typekit.net10xfotolia.com 2o7.net acrobat.com adbecrsl.comadobe-aemassets-value.comadobe-audience-finder.com"adobe-video-partner-finder.com  adobe.com adobe.lyadobeaemcloud.comadobeaemcloud.netadobeawards.comadobecreativityawards.com adobedc.net adobedemo.com adobedtm.comadobeexchange.comadobeexperienceawards.com adobegov.comadobehiddentreasures.comadobejanus.com adobeku.comadobelogin.com adobeoobe.comadobeplatinumclub.comadobeprojectm.com adobesc.com adobesign.comadobesigncdn.comadobespark.com adobess.comadobestock.com adobetag.comadobetarget.comadobetcstrialdvd.comadobetechcomm.comadobetechcommcallback.comadobetechcommdemo.comadobexdplatform.comassetsadobe.com authorxml.combusinesscatalyst.com ccnsite.com  ccpsx.com -demdex.netdollarfotoclub.comdollarphotoclub.comdollarphotosclub.comdouwriteright.com echocdn.com echosign.com edgefonts.netenablementadobe.com  ftcdn.net gostorego.comimagineecommerce.commacromedia.commagentoliveconference.commarketing-nirvana.com omniture.com -omtrdc.net photoshop.complacesdocs.comsundanceignite2016.comworldsecureemail.comworldsecuresystems.com adobe.iodevelopria.com behance.nets2stagehance.com adobecc.com adobecce.comadobeccstatic.comcreativecloud.comcreativesdk.com cotolia.com ffotolia.com fiotolia.com foftolia.com fonolia.com fotiolia.com fotoiia.comfotolia-noticias.com -fotolia.cc fotolia.com -fotolia.tv fotolja.com fptolia.com gfotolia.com motolia.com photolia.nettenbyfotolia.com votolia.combluefootcms.com mageconf.commageconf.com.ua magento.com magento.netmagentocommerce.commagentomobile.commarketing-cloud.com marketo.co.uk marketo.com marketo.net -marketo.tvmarketodesigner.commarketolive.com -mktdns.commkto-c0100.com mktorest.com mktroute.commobilemarketo.com&"revenue-performance-management.com toutapp.com -F -COINONE coinone.co.krcoinonecore.comcoinonecorp.com - -DISNEYabc-studios.com abc.com edgedatg.com espn.com espncdn.com watchespn.comespn.hb.omtrdc.netespndotcom.tt.omtrdc.net -marvel.commarvel10thanniversary.commarveldimensionofheroes.commarvelparty.netmarvelpinball.commarvelsdoubleagent.commarvelspotlightplays.com marvelsuperheroseptember.commarvelsuperwar.comthemarvelexperiencetour.comnatgeomaps.comnationalgeographic.com"nationalgeographicpartners.com ngeo.comadventuresbydisney.com -babble.com -dilcdn.comdisney-asia.comdisney-discount.comdisney-plus.netdisney-studio.comdisney-studio.net  disney.ca -disney.com disney.com.br  disney.iodisneyadsales.comdisneyarena.comdisneyaulani.comdisneybaby.comdisneycareers.comdisneychannelonstage.comdisneychannelroadtrip.comdisneycruisebrasil.comdisneyenconcert.comdisneyiejobs.comdisneyinflight.comdisneyjunior.comdisneyjuniortreataday.comdisneylatino.comdisneymeetingsandevents.comdisneymovieinsiders.comdisneymusicpromotion.comdisneynewseries.com disneynow.comdisneypeoplesurveys.comdisneyplus.comdisneyredirects.comdisneysrivieraresort.comdisneystore.comdisneysubscription.comdisneytvajobs.comdisneyworld-go.com -dssott.comgo-disneyworldgo.com -go.com  mickey.tvmoviesanywhere.complaymation.comshopdisney.comshops-disney.comsorcerersarena.comspaindisney.com starwars.com#starwarsgalacticstarcruiser.comstreamingdisney.netthestationbymaker.comthisispolaris.comwatchdisneyfe.com - -XHAMSTER xhamster.comxhamsterlive.com  xhcdn.comwidgets.stripst.com#collector.xhamster.com -ads - -QMAP qmap.pub -" - TYPENETWORKtypenetwork.com - -WHONIX -whonix.org -. -XUEQIUdanjuanfunds.com -xueqiu.com -Z -XUNLEI  00cdn.com  88cdn.com -sandai.netthunderurl.com -xunlei.com - -ICLOUDapple-icloud.cn -cnappleicloud.cn -cnapplemx-icloud.comicloud-apple.cn -cnicloud-content.comicloud-isupport.com  icloud.ch -icloud.com icloud.com.cn -cn  icloud.de  icloud.ee  icloud.fi  icloud.fr  icloud.hu  icloud.ie  icloud.is  icloud.jp  icloud.lv icloud.net.cn -cn  icloud.om -icloud.org  icloud.pt  icloud.ro  icloud.se  icloud.si  icloud.sk  icloud.vn icloudads.neticloudapple.cn -cn icloudbox.neticloudbrowser.net icloude.comicloudhome.comicloudmail.neticloudmusic.net icloudnet.net icloudpay.net icloudo.com -icloudo.de icloudo.net icloudos.de icloudos.neticloudsecure.neticloudsetup.comios-icloud.com -me.com myicloud.net mylcloud.net wwwicloud.com!www-cdn.icloud.com.akadns.net -9 -MOGUJIE mogu.com mogucdn.com mogujie.com - -RIOThistoryofdota.comhistoryofdota.nethistoryofdota.orginstituteofwar.orgmolesports.com rgpub.ioriot-games.com riot.com riot.net riotcdn.netriotgames.co.kr riotgames.comriotgames.info riotgames.jp riotgames.net riotgames.tv riotpin.comriotpoints.com rstatic.net supremacy.com supremacy.net championshipseriesleague.com lcsmerch.comleaguehighschool.comleagueoflegends.caleagueoflegends.cn -cnleagueoflegends.co.krleagueoflegends.comleagueoflegends.infoleagueoflegends.krleagueoflegends.netleagueoflegends.orgleagueoflegendsscripts.comleaguesharp.infoleaguoflegends.comlearnwithleague.comlol-europe.com lolclub.org lolespor.comlolesports.com lolfanart.net -lolpcs.com lolshop.co.kr lolstatic.comlolusercontent.com -lpl.com.cn -cn pvp.net -pvp.tv ulol.comlolstatic-a.akamaihd.net -$ -INTEL -01.org hyperscan.iointellinuxgraphics.comintellinuxgraphics.net -oneapi.comopenvinotoolkit.orgsnap-telemetry.io -acpica.com clearlinux.cn -cnclearlinux.org -ospray.net -ospray.org intel intel.ac intel.ae intel.af intel.ag intel.ai intel.ar intel.at intel.az intel.ba intel.bg intel.bh intel.bi intel.bo intel.bs intel.by intel.ca intel.cc intel.cg intel.ch intel.cl intel.cmintel.cn -cn intel.co.ae intel.co.cr intel.co.id intel.co.il intel.co.jp intel.co.kr intel.co.uk intel.co.za  intel.com intel.com.ar intel.com.au intel.com.bo intel.com.br intel.com.cn -cn intel.com.co intel.com.ec intel.com.hk intel.com.jm intel.com.mx intel.com.my intel.com.pe intel.com.ph intel.com.pr intel.com.py intel.com.tr intel.com.tw intel.com.uy intel.com.ve intel.cr intel.cu intel.cz intel.de intel.dk intel.dz intel.ec intel.ee intel.eg intel.es intel.eu intel.fi intel.fr intel.ga intel.gd intel.ge intel.gl intel.gm intel.gr intel.gs intel.gt intel.gy intel.hk intel.hn intel.ht intel.hu intel.ie intel.in intel.io intel.it intel.je intel.jo intel.jp intel.ke intel.la intel.lc intel.lk intel.lt intel.lu intel.ly intel.ma intel.md intel.me intel.mg intel.mk intel.mn intel.mp intel.mt intel.mu intel.mw intel.mx intel.my intel.ng intel.nl intel.nu intel.nz intel.pa intel.pe intel.ph intel.pl intel.pn intel.re intel.ro intel.ru intel.rw intel.sa intel.sc intel.se intel.sg intel.si intel.sk intel.sn intel.sr intel.st intel.sv intel.sx intel.sy intel.tf intel.tj intel.tl intel.tm intel.tn intel.tt intel.tv intel.tw intel.ua intel.uk intel.us intel.uy intel.uz intel.vg intel.vn intel.vu intel.wf intel.yt -altera.comalteraforum.comalteraforums.comalteraforums.netalterauserforum.comalterauserforum.netalterauserforums.comalterauserforums.net buyaltera.com celeron.com celeron.net centrino.com centrino.net  chips.com cilk.com cilk.netcloudinsights.comclusterconnection.com coreduo.comcoreextreme.comcrosswalk-project.comcrosswalk-project.netdoceapower.com  easic.com enpirion.comexascale-tech.comexploreintel.comgordonmoore.cominsidefilms.com intc.comintel-research.net&"intel-university-collaboration.netintelamericasstore.comintelapacstore.com intelatom.netintelcapital.comintelcapital.netintelcloudbuilders.comintelcloudfinder.comintelemeastore.cominteleventexpress.comintelforchange.comintelfreepress.com intelgo.netintelinsight.cominteliotmarketplace.com -intell.comintellearningseries.comintellinuxwireless.netintelnervana.comintelnet.component intelplay.comintelquark.comintelrealsense.cn -cnintelrealsense.com intelrxt.comintelsalestraining.comintelsecurity.comintelserveredge.comintelsoftwarenetwork.comintelstore.cominteltechnologyprovider.com intelvmwarecybersecurity.com  itnel.comlatencytop.comlookinside.commakebettercode.commakesenseofdata.com movidius.com movidius.netnervanasys.com  nevex.comnextgenerationcenter.com -niosii.com -niosii.netomekinteractive.com -omnitek.tv openamt.comopendroneid.orgoptanedifference.com -pc.com pentium.com pentium.net pintool.compoweredbyintel.comreconinstruments.com reconjet.comresearchintel.comsaffrontech.comsensorynetworks.com -siport.comsmart-edge.comtheintelstore.comthunderbolttechnology.nettrustedanalytics.comtrustedanalytics.net -vokevr.com vpro.com vpro.net xeon.comxn--ztsq84g.cn -xscale.combarefootnetworks.com - -SINA  97973.com -picself.cn sina.cn sina.com sina.com.cn sina.net sinaapp.com sinaedge.com -sinaimg.cn sinaimg.com  sinajs.cn -sinajs.comt.cn videoself.cn  wbimg.com wbimg.cn wcdn.cn weibo.cn  weibo.com weibo.com.cn weibocdn.com xhaiwai.com 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -ads -D - -POCOIQ-ADS cdn.pocoiq.cn -ads oct.pocoiq.cn -ads - -VERISIGN verisign.asia verisign.biz verisign.chverisign.co.inverisign.co.uk verisign.comverisign.com.auverisign.com.brverisign.com.cnverisign.com.esverisign.com.hkverisign.com.sgverisign.com.twverisign.com.vn verisign.de verisign.dk verisign.es verisign.fr verisign.hk verisign.in verisign.info verisign.jobs verisign.mobi verisign.name verisign.net verisign.org verisign.pro verisign.se verisign.sg verisign.tw verisign.us verisign.vnverisigninc.com -5 -COLORFULCLOUDS caiyunai.com caiyunapp.com -% - FONTEXPLORERfontexplorerx.com - -GLYPHS glyphsapp.com - - -FAMILYMARTfamifun.com.tw family.co.jp family.com.twfamilymart.com.cn -cnfamilymart.com.myfamilymart.com.ph  famima.vn - -WALMART walmart wal-mart.com -wal.cowalmart-content.com walmart.comwalmart.pharmacywalmartimages.com -wmt.co - -OVERCAST overcast.fm -f -COURSERAcoursera-for-business.orgcoursera.community coursera.help coursera.org -2 -INTERNATIONALSAIMOEinternationalsaimoe.com - -MACPORTS macports.org - -BADGEN -badgen.net -" - SHADOWSOCKSshadowsocks.org -; - SCORELAND scoreland.comcdn77.scoreuniverse.com - - -BLOGSPOT blogspot.ae blogspot.al blogspot.am blogspot.ba blogspot.be blogspot.bg blogspot.bj blogspot.ca blogspot.cf blogspot.ch blogspot.clblogspot.co.atblogspot.co.idblogspot.co.ilblogspot.co.keblogspot.co.nzblogspot.co.ukblogspot.co.za blogspot.comblogspot.com.arblogspot.com.aublogspot.com.brblogspot.com.byblogspot.com.coblogspot.com.cyblogspot.com.eeblogspot.com.egblogspot.com.esblogspot.com.mtblogspot.com.ngblogspot.com.trblogspot.com.uy blogspot.cv blogspot.cz blogspot.de blogspot.dk blogspot.fi blogspot.fr blogspot.gr blogspot.hk blogspot.hr blogspot.hu blogspot.ie blogspot.in blogspot.is blogspot.it blogspot.jp blogspot.kr blogspot.li blogspot.lt blogspot.lu blogspot.md blogspot.mk blogspot.mx blogspot.my blogspot.nl blogspot.no blogspot.pe blogspot.pt blogspot.qa blogspot.re blogspot.ro blogspot.rs blogspot.ru blogspot.se blogspot.sg blogspot.si blogspot.sk blogspot.sn blogspot.td blogspot.tw blogspot.ug blogspot.vn - -ELREPO -elrepo.org - -GITHUB atom.io  npmjs.com npm.communitydependabot.com -git.io github.blog -github.comgithub.community  github.io githubapp.comgithubassets.comgithubhackathon.comgithubstatus.comgithubusercontent.com/+github-avatars.oss-cn-hongkong.aliyuncs.comFBgithub-production-release-asset-[0-9a-za-z]{6}\.s3\.amazonaws\.com - -SSRPASS -ssrpass.pw - - WIKIMEDIA mediawiki.org wikibooks.org wikidata.orgwikimediafoundation.org wikimedia.org wikinews.org wikipedia.org wikiquote.orgwikisource.orgwikiversity.orgwikivoyage.orgwiktionary.orgwmfusercontent.org -w.wiki toolforge.orgwikimediacloud.org wmcloud.org wmflabs.org - -YYETS -got001.com -got002.com jstucdn.com playcvn.com rrys.tv rrys2019.com rrysapp.com  yyets.com zmz2019.com -zmzapi.com cvnad.com -ads -iyoowi.com -ads - -BITCOIN bitcoin.org -ҍ -GEOLOCATION-CN -42r.cn -5566ua.com aurorapush.cn -e0n.cn japps.cn jchat.io -jiguang.cn jmlk.co jpush.cn jpush.io jpushoa.com -jsurvey.cn  jvoice.cn -kc9.cn -n0q.cn pushcfg.com -s0n.cn mobmore.com  umeng.comumengcloud.com  umsns.com umtrack.com -uyunad.comwhalecloud.comalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads bootcss.com -bootcdn.cnphpcomposer.com -maoyun.commaoyuncloud.cnmaoyuncloud.comstaticfile.org qbox.me  qiniu.com qiniucdn.com qiniudn.com qiniudns.com  qnssl.com  ucloud.cn ufileos.com  aicdn.com upai.com upaiyun.com  upcdn.net  upyun.com  8686c.comchinanetcenter.com -wangsu.com ourdvsss.com yfcache.com -yfcalc.com yfcloud.com  yfdts.net  yfp2p.net -yfscdn.net yunfancdn.com -boc.cnbankofchina.combocaviation.com  boccfc.cn  bocgi.com bocgins.com  bochk.com bocichina.com bocigroup.com  bocim.combocservices.com.cn cignacmb.comcmb-leasing.com cmbchina.com cmbi.com.hkcmbwinglungbank.comicbc icbc-ltd.com icbc-us.com icbc.com.cn  95516.com unionpay.comunionpayintl.comunionpaysecure.com abchina.com  whccb.com cebbank.com ceblease.com -eb-ind.com ebasset.com ebchina.comebchinaintl.com -ebf.com.cn  ebfcn.com ebfortune.com ebtrust.com ebscn-am.com  ebscn.comeverbright.com  secec.comsunlife-everbright.com taikang.comtaikanglife.comtaikangzhijia.comtkhealthcare.comdanjuanfunds.com -xueqiu.com -58.com  58.com.cn 5858.com  58che.com 58xueche.com -anjuke.comanjukestatic.com chinahr.com -daojia.com diandao.org fang.com  sofun.com soufunimg.com  jxedt.com zhuancorp.comzhuanspirit.comzhuanzhuan.com c-ctrip.com colinker.com ctrip.cn ctrip.co.id ctrip.co.kr ctrip.co.th  ctrip.com ctrip.com.hk ctrip.my ctrip.sg ctripbiz.com ctripbuy.hk ctripteam.com doticloud.com easytrip.com hhtravel.comhhtravel.com.tw izuchebao.com  lvtds.com  shuntu.cn -skysea.com  suanya.cn -suanya.com -tieyou.com -toptown.cn toursbms.com trip.com tripcdn.com -vipdlt.com dangdang.com ddimg.cn -ele.me elemecdn.com eleme.cn  ganji.comganjistatic1.com3.cn  300hu.com -360buy.com 360buyimg.com -360top.com -7fresh.com baitiao.comblackdragon.com  caiyu.comchinabank.com.cn -dao123.comjcloud-cdn.comjcloud-live.comjcloud-oss.com -jcloud.comjcloudcache.com jcloudcs.com jclouddn.com jcloudec.com jcloudlb.comjcloudlive.com jcloudlv.com jcloudoss.com jcloudss.comjcloudstatic.comjcloudvideo.com  jclps.com -jd-app.com  jd-ex.com jd.cn jd.co.th -jd.com jd.hk jd.id jd.ru jdcache.com jdcloud.com jdcloudcs.comjdcloud-api.comjddapeigou.com jddebug.com jddglobal.com jdjinrong.com  jdpay.com jdpaydns.com jdx.com jdwl.comjingdongjinrong.com -jingxi.com -jkcsjd.com -joybuy.com  joybuy.eslinglonglife.com mayshijia.com minitiao.com  ocwms.com paidaojia.cn -paipai.comprestodb-china.com qianxun.comqingzhouip.com toplife.com -vg.com wangyin.com  wdfok.com yhd.com yihaodian.comyihaodianimg.com yiyaojd.com yizhitou.comads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads lianjia.com  ljcdn.com manmanbuy.com dianping.com -maoyan.com meituan.com meituan.net  mtyun.com  neixin.cn sankuai.com mogu.com mogucdn.com mogujie.com -pddpic.com pinduoduo.com yangkeduo.com  smzdm.com -zdmimg.com cnsuning.com -suning.comsuningcloud.comsuningestate.comsuningholdings.com 40017.cn -17u.cn 17u.net 17usoft.com 17usoft.net  elong.comelongstatic.com -ly.com  vancl.com vanclimg.com  vcimg.com -youzan.com yzcdn.cn youzanyun.com -ziroom.comziroomapartment.com 3dmgame.com 3304399.net 4399.com 4399api.com 4399dmw.com -4399er.com4399youpai.com 5054399.com img4399.com acfun.cn aixifan.com apilog-web.acfun.cn -ads -acplay.netdandanplay.com -ali213.net -acg.tv acgvideo.com -b23.tv biliapi.com biliapi.net bilibili.combilibiligame.net biligame.com biligame.net bilivideo.com  hdslb.com im9.com -mincdn.com$ upos-hz-mirrorakam.akamaized.net biji.com -ddmogo.com -igetget.cn igetget.com luojilab.com  mogoo.com -sao.cn saolife.com dmzj.com  douyu.com douyu.tv douyucdn.cn douyucdn2.cn douyuscdn.com douyutv.com gamersky.com -gcores.com gitv.cn gitv.tv huya.com msstatic.com 71.am  iqiyi.com iqiyipic.com -pps.tv -ppsimg.com qiyi.com qiyipic.com -qy.net -71edge.coma.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -ads  jjwxc.net ku6.com -ku6cdn.com -ku6img.com kkmh.comkuaikanmanhua.com gifshow.com ksapisrv.com kuaishou.com -yximgs.com log-sdk.gifshow.com -adswlog.kuaishou.com -ads  kgimg.com  kugou.com -le.com lecloud.com -lemall.com letv.com letvcdn.com letvimg.com miaopai.com -mihoyo.com bh3.com  pplive.cn -pplive.com pptv.com qingting.fm  sfacg.com -61.com -taomee.com -tapimg.com -taptap.com  tgbus.com tiancity.comtiancitycdn.com u17.com u17i.com u17t.com -vgtime.com ximalaya.com  xmcdn.com -kumiao.com  youku.com  ykimg.com -mmstat.com soku.com -cibntv.net -duowan.com dwstatic.com -yy.com yystatic.com -heytea.com chiphell.com -douban.com  douban.fm doubanio.com erji.net -hacpai.com -nga.cn 178.com -ngabbs.com ngacn.cc saraba1st.com  zhihu.com  zhimg.com  feng1.comrongechain.comsf-airlines.comsf-cityrush.comsf-express.comsf-financial.com sf-saas.comsf-tech.com.cn sfcar.hk sffix.cn sfgy.org 36kr.com 36krcdn.com  dgtle.comhoopchina.com.cn hupu.com hupucdn.com  shihuo.cn coolbuy.com  ifanr.com ifanr.inifanrusercontent.com -ifanrx.com -minapp.com -caixin.com cctv.cn cctv.com cctvpic.com chinaepg.cn chinalive.com citv.net.cn cnms.net.cn cntv.cn cntv.com.cn -cctvlib.cncctvlibrary.cncctvlib.com.cncctvlibrary.com.cn -cctvpro.cncctvpro.com.cn  cntvwb.cn gjgbdszt.cngjgbdszt.com.cngjgbdszt.net.cn  ipanda.cn -ipanda.com ipanda.com.cn -ipanda.net livechina.cn livechina.comolympicchannelchina.cn tvcc.cn tvcc.com.cn%!xn--fiq53l6wcx3kp9bc7joo6apn8a.cn%!xn--fiq53l90et9fpncc7joo6apn8a.cn-)xn--fiq53l6wcx3kp9bc7joo6apn8a.xn--fiqs8s%!xn--kprv4ewxfr9cpxcc7joo6apn8a.cn-)xn--kprv4ewxfr9cpxcc7joo6apn8a.xn--fiqs8s zggbdszt.cnzggbdszt.com.cnzggbdszt.net.cnzygbdszt.net.cn chinanews.comchinanews.com.cn -cns.com.cn ecns.cn geekpark.net jiemian.comjiemian.com.cn -lancdn.comlandiannews.com  wsoso.com  ifeng.com ifengimg.comphoenixcenter.cn fengedu.comifengweekly.com 6655.com cooldock.com -ithome.com lapin365.com ruanmei.com -saayaa.com win7china.com win8china.com sohu.com -sohucs.com -itc.cn solidot.org  sspai.com tmtpost.com chaindd.com -72whys.com checkzeit.com -tivitv.com tigeek.net.cntmtsmartlife.comtmtsmartrun.com feng.com fengimg.com wfdata.club ynet.cn ynet.com ynet.com.cnhainanairlines.com hnacargo.com  hnair.com 10044.cnsinopec-usa.com sinopec.comsinopecgroup.comsinopecnews.com.cnsinopecsales.com -csdl.ac.cn  las.ac.cn  fuwai.com pkuh6.cn puh3.net.cnhospital.pku.edu.cnss.bjmu.edu.cn 115.com 3type.cn 4paradigm.com  51job.com 51jobcdn.comyingjiesheng.comyingjiesheng.netzhiding.com.cn -afdian.net afdiancdn.com agora.io -agoraio.cn agoralab.co -antutu.com bitauto.combitautoimg.com cheyisou.com  yiche.com  yccdn.com shimo.im  yuque.com chinaso.com -pg0.cn -chinaz.com -chinaz.net caiyunai.com caiyunapp.com coolapk.comcoolapkmarket.comcoolapkmarket.net c-t.workcowtransfer.com geetest.com#geetest.datasink.sensorsdata.cn hellofont.cnhellowebfont.com0,hellofontpreview.oss-cn-beijing.aliyuncs.com-)hellowebfonts.oss-cn-beijing.aliyuncs.com hanyi.com.cnfontstar.com.cnfoundertype.com ipip.net jianshu.com -jianshu.io jinshuju.netjinshujucdn.com gotokeep.com keepcdn.com  lagou.com lgstatic.com baidupan.com -lanzou.com lanzous.com woozooo.com -liepin.com -lietou.comlietou-static.com mafengwo.cn mafengwo.com mafengwo.net  meitu.com meitudata.com moji.com mojichina.com  polyv.net videocc.net -renren.comrenren-inc.com vaptcha.com vaptcha.net wamawama.com -wiwide.com -sojump.com -wjx.cn wjx.com wjx.top zcmbc.com.cn zdns.cn zdns.net.cn zdns.org.cn zdnscloud.biz zdnscloud.cn zdnscloud.comzdnscloud.com.cnzdnscloud.info zdnscloud.netzdnscloud.net.cnzdnscloud.org.cnchinesemooc.org -cnmooc.orgicourse163.org icourses.cn mooc.cnopenlearning.com pmphmooc.commoocs.unipus.cnopencourse.pku.edu.cn study.163.com cnki.net  apabi.comcnthinkers.com  cqvip.comhuijistatic.com huijiwiki.com moegirl.orgmoegirl.org.cn  guokr.comwanfangdata.com.cn speiyou.com -xesimg.com xueersi.com banmaaike.comxiaoyuankousuan.com ybccode.com yuanfudao.com yuansouti.com yuantiku.com zhihuishu.com  coding.me -coding.net csdn.net -csdnimg.cn  deepin.cn -deepin.com  deepin.io -deepin.org  gitee.comjinrishici.com  juejin.imb-gold-cdn.xitu.iomirror-gold-cdn.xitu.iop1-juejin.byteimg.comp2-juejin.byteimg.comp3-juejin.byteimg.comp4-juejin.byteimg.comp5-juejin.byteimg.comp6-juejin.byteimg.comp7-juejin.byteimg.comp8-juejin.byteimg.comp9-juejin.byteimg.com oschina.netruby-china.orgrubyconfchina.org mysxl.cn -sxl.cn  sxlcdn.cnsegmentfault.com  sifou.com -tipdim.org alibaba -alipay -taobao tmall a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -adsalibabacloud.co.inalibabacloud.comalibabacloud.com.aualibabacloud.com.hkalibabacloud.com.myalibabacloud.com.sgalibabacloud.com.tw alicloud.comalibabadns.com -alicdn.com alidayu.com -alidns.comaliyun-inc.comaliyun-iot-share.com -aliyun.com aliyuncdn.com aliyuncs.com hichina.comica-alliance.org mxhichina.com ddurl.to dingding.xin dingtalk.cn dingtalk.com dingtalk.netdingtalkapps.com laiwang.com lwurl.toteambition.comteambition.netteambitionapis.com 25pp.com  56che.com  95095.com 9game.cn -aliapp.orgalibaba-inc.com alibaba.comalibabacapital.comalibabacorp.comalibabadoctor.comalibabafuturehotel.comalibabagroup.comalibabaplanet.comalibabaued.comalibabausercontent.com alifanyi.comalihealth.com.cn alihealth.hk -aliimg.com -alikmd.com alimama.com alimebot.com -alimei.com alios.cn -alipay.comalipayobjects.com aliplus.comaliresearch.com alisoft.com alisports.com alitianji.comaliunicorn.com -aliway.com aliwork.com alixiaomi.comalizhaopin.com alyms.cn -asczwa.comasczxcefsv.com -asmlink.cn atatech.org -b2byao.com  bazai.com  bcvbw.com  cheng.xin cloudeagle.cn damai.cn dayu.com -dfkhgj.com doctoryou.ai doctoryou.cn dongting.com -dratio.com emas-poc.com -gongyi.xin ialicdn.com iconfont.cn -ics.design -jhgtgb.com -kanbox.com -lazada.comliangxinyao.com m-now.cn -maitix.com  mdeer.com mei.commiaostreet.com  mshare.cc nic.xinphilanthropy.xin phpwind.com phpwind.netqingtingtrip.com  rexpos.cn saee.org.cn shenjing.com  shuqi.comshuqireader.com sparenode.com  supet.com  ttpod.com wandoujia.comxinlingshou.cn yowhale.com yunos-inc.com  yunos.comyushanfang.com -zuodao.comaliexpress.com aliexpress.ru sm.cn cainiao.comcainiao.com.cncainiao-inc.com freshhema.com hemamax.com -hemaos.com hemashare.cn shyhhema.com 1688.com etao.com juhuasuan.comlingshoujia.com -mashort.cn pailitao.com -taobao.com -taobao.org taobaocdn.comtaobizhong.comtaopiaopiao.com tb.cn tbcache.com tburl.in  tmall.com tmall.ru tmalltv.com tmjl.ai alitrip.com  feizhu.cn -feizhu.com -fliggy.com  fliggy.hk -iuynfg.com amap.com amapauto.com autonavi.com  gaode.com  xiami.com xiami.fm  xiami.net  b3log.org a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -ads zuoyebang.cc zuoyebang.com -zybang.com baidu -91.com -aipage.com apollo.auto baidu.cn  baidu.com baidu.com.cn baidubce.combaiducontent.com baidupcs.combaidustatic.com -baifae.com baifubao.com -bcebos.com  bdimg.com bdstatic.com bdtjrcv.com  bdydns.cn -bdydns.com -bdydns.net chuanke.com  dlnel.com  dlnel.org -duapps.com -dwz.cn -hao123.com hao123img.com -hao222.com -haokan.com jomocdn.net jomodns.com -mipcdn.com  nuomi.com quyaoya.com smartapps.cn  tieba.com tiebaimg.com xianfae.com xiaodutv.com -bytecdn.cn bytedance.com bytedance.net bytedns.net byteimg.com -douyin.com huaxiong.comjokecommunity.comneihanshequ.com -pstatp.com -snssdk.com toutiao.comtoutiaocdn.comtoutiaocloud.comtoutiaohao.comtoutiaohao.net toutiaoimg.cntoutiaoimg.comtoutiaoimg.nettoutiaopage.com -wukong.comwukongwenda.cnwukongwenda.com zijiecdn.com zijiecdn.net  zjcdn.com  baike.com -ixigua.com ixgvideo.com  365yg.com didi-food.comdidiaustralia.blogdidichuxing.comdidiglobal.comdidimobility.co.jpdidistatic.comxiaojukeji.com  dbank.com dbankcdn.com harmonyos.com hicloud.com hihonor.com honor.cn -huawei.comhuaweicloud.com  vmall.com vmallres.com cmcm.com cmcmcdn.com  liebao.cn duba.com duba.net ijinshan.comseasungames.com xishanju.com xoyo.com testplus.cn -wps.cn  51dzt.com  iciba.com  docer.com kdocs.cn papocket.com  wpspdf.cn kimg.cn kingsoft.com ksmobile.com -ks-cdn.com  ksyun.com ksyungslb.com flyme.cn meizu.cn  meizu.com  mzres.com 126.com 126.link 126.net 127.com 127.net 163.com163industry.com 163mail.com 163qiyukf.com -163yun.com 166.net -haowu.linkicourse163.org  kaola.com -lofter.com  nease.netnetease-inc.com netease.com -netease.im -qiyukf.com ydstatic.com yeah.net -youdao.comanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads coloros.com finzfin.com -heytap.com heytapcs.comheytapdownload.comheytapimage.com -myoppo.com nearme.com.cn oppo.cn oppo.com  oppo.mobioppodigital.com  oppoer.meoppomobile.com oppopay.com opposhop.cn -realme.com realmebbs.comrealmemobile.com oneplus.com -oneplus.cn oneplus.net opstatics.comoneplusbbs.com h2os.com 75.team -75team.com baomitu.com -360.cn 360.com -360-jr.com 360chou.com360daikuan.com360huzhubao.com -360jie.com360jinrong.net -360kan.com 360kuai.com  360os.com 360panyun.cn 360safe.com 360shouji.com 360taojin.com360totalsecurity.com 360tpcdn.com360webcache.com anquanke.com  haoso.com -haosou.com haosou.com.cn -haosou.net i360mall.com ludashi.com nicaifu.com  nzwgs.com  qhimg.com qhimgs0.com qhimgs1.com qhimgs2.com qhimgs3.com qhimgs4.com qhimgs5.com qhimgs6.com  qhmsg.com  qhres.com qhstatic.com qhupdate.com -qihuapi.cn qihucdn.com  qihoo.com qikucdn.com -so.com sou.com urlqh.cnxiaoluyouxuan.comxiaoluzhidian.com xjietiao.com -youhua.com  yunpan.cn  97973.com -picself.cn sina.cn sina.com sina.com.cn sina.net sinaapp.com sinaedge.com -sinaimg.cn sinaimg.com  sinajs.cn -sinajs.comt.cn videoself.cn  wbimg.com wbimg.cn wcdn.cn weibo.cn  weibo.com weibo.com.cn weibocdn.com xhaiwai.com 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -ads -go2map.com sogo.com  sogou.com sogoucdn.com dns.pub  dnsapi.cn  dnspod.cn -dnspod.com dnspod.com.cn -dnspod.net -dnspod.org  dnsv1.com  dnsv2.com  dnsv3.com  dnsv4.com  dnsv5.com doh.pub ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads -yuewen.com hongxiu.com lrts.me qdmm.com -qidian.com readnovel.com tingbook.com webnovel.com -xs8.cn xxsy.net -apcdns.net  cdn-go.cn cdntips.com foxmail.comfoxmail.com.cn gtimg.cn  gtimg.com gtimg.com.cn idqqimg.com imqq.com  myapp.com myqcloud.com -qcloud.com qcloudimg.com qlogo.cn qpic.cn -qq.com -qqmail.com  qzone.com -rtx.com.cnservicewechat.com smtcdns.com smtcdns.net -tencent.cn tencent.comtencent.com.cntencent.com.hktencent.net.cntencent-cloud.comtencentmusic.comtencentyun.com -tenpay.com -wechat.com -wegame.com wegame.com.cn -weiyun.com alloyteam.com imweb.io ivweb.io -mi.com -mi-img.com  mifile.cn miui.com -miwifi.com  xiaomi.cn -xiaomi.com -xiaomi.netxiaomiyoupin.com  00cdn.com  88cdn.com -sandai.netthunderurl.com -xunlei.com 10086.cn 139.comchinamobile.comchinamobileltd.com migucloud.com migu.cn -cmvideo.cn miguvideo.com andfx.cn  andfx.net -cmicrwx.cncmpassport.comfetion-portal.com fetionpic.com mmarket.com mmarket6.com -ctexcel.ca ctexcel.comctexcel.com.hk -ctexcel.fr -ctexcel.us -189.cnchinatelecom-h.comchinatelecom.com.cnchinatelecomglobal.com -chntel.com  10010.com 10010.com.cnchinaunicom.comchinaunicom.com.cnchinaunicom.com.hkchinaunicomglobal.commychinaunicom.com  wo.com.cnguoxuedashi.com guoxuemi.com jikipedia.com haidilao.com0123456789.com 0379home.com -055110.com  05sun.com 0759job.com  100bt.com 100offer.com  11467.com -114chn.com 114piaowu.com 114shouji.com -119you.com 123juzi.com 123juzi.net 123u.com 1637.com  16888.com 16rd.com  17173.com 178linux.com  17989.com  17cdn.com 17house.com  18183.com 188.com 1905.com  19yxw.com 1nongjing.com  1ting.com 1zhe.com 21cn.com 2265.com 2288.org 2345.com 258.com 263.net 289.com 2cto.com 3000.com -310win.com -315che.com 3234.com 32r.com 3322.org 3387.com 33lc.com 3454.com 3456.cc -35.com 3533.com 360aiyi.com -360doc.com  360jq.com 360zhyx.com -365azw.com -365dmp.com -37.com 3733.com 3839.com -39.net 3conline.com 3d66.com -3elife.net 3h3.com 3yx.com  40407.com 500.com 500d.me -50bang.org -51.com 51.la -510you.com 51credit.com  51cto.com 51eshop.com 51gaifang.com 51hejia.com51jingying.com 51meishu.com -51sole.com 51wendang.com  51yes.com 5253.com  52hxw.com -52miji.com 52pk.com 52z.com 53kf.com -55.com -56.com 5577.com  55bbs.com  58pic.com  5bite.com 5fun.com 5gzm.net 5khouse.com 5you.com 66wz.com 68h5.com -6rooms.com 714.com 7230.com -72byte.com  7down.com 7k7k.com  7moor.com 7po.com -7x24cc.com -7xdown.com  818ps.com  84399.com 885.com -900.la 917.com 91danji.com 91yunxiao.com 9553.com 962.net 9718.com  99166.com 998.com 999d.com 99danji.com 9ht.com -9xgame.com 9xu.com a9vg.com -aardio.com -adxvip.com -afzhan.com agrantsem.com  aiemy.com aiketour.com  aiops.com  aipai.comairchangan.com airguilin.com aisee.tv aiskycn.com aiyunxiao.com -aizhan.comalittle-tea.com -allyes.com anfensi.com -anquan.org -anruan.com -aotrip.net  aoyou.com apk3.com apk8.com appchina.com -appinn.com  arpun.com babytree.combabytreeimg.com baidufree.combaifendian.com  baihe.com baixing.com baixing.netbamenzhushou.combangongziyuan.com bankcomm.com baofeng.combaoxianshichang.com bcy.net bearychat.com beihaidc.combeijing-lipin.com bengbeng.com  benmi.com  berui.combestopview.com biancheng.net bianews.com bibaodao.com bidchance.com biddingx.com bihu.com -bio360.net  bio4p.com bio-equip.combiodiscover.com bishijie.com bitecoin.com biyehome.net bjcathay.com bobo.com bojianger.com -bokecc.com  boosj.com btcfans.com btgtravel.com bthhotels.com  btime.comcabbagebox.com caijingwu.com -caiyun.com cang.comcankaoxiaoxi.comcardbaobao.com -carnoc.com ccb.com  cdn20.com  cecdc.com cecport.com centanet.com cerambath.org -ch.com chachaba.com changba.com chashebao.comchazhengla.com -che168.comchina-chair.comchina-nengyuan.com  china.comchinacache.comchinacache.netchinachugui.comchinadongchu.com chinafph.comchinakaoyan.comchinalawedu.com chinapay.com chinapp.netchinapubmed.net chinasspp.comchinatranslation.net chinaunix.netchinaweiyu.comchinawutong.com chinazns.comchongdiantou.com -chouti.com chuangxin.com chuansong.mechuansongme.com -chushou.tv chuyu.me  city8.com cjol.com clouddn.com cloudxns.com cnanzhi.com -cnbeta.com cnbetacdn.com cnblogs.com -cnepub.com  cnfol.com  cnlaw.net cnstock.com cnzz.com cocodiy.com code666.com -cofeed.com -cofool.com coindog.comcontentchina.com  cpppf.org  cr173.com  crsky.com  csbew.com -csmjzs.com csslcloud.net -ctfile.com ctiforum.com ctoutiao.com  ctsbw.com -cubead.com  cwxzx.com -cyagen.com  cybtc.com cyol.com cyts.com cztv.com cztvcloud.com dadajiasu.com dadighost.com daguzhe.com dajiabao.com  dajie.com dajieimg.com dalongyun.com dangbei.com danji100.com -danji6.com daocloud.io daovoice.iodayukaoshi.com dazijia.com -dcdkjx.com  dd373.com  ddooo.com dedecms.com develenv.com -dginfo.comdgphospital.comdianchacha.com diandian.comdiandianqi.comdianwannan.comdingxiang-inc.com -diopic.net diyiyou.com dj99.com  docin.com dockerone.com -dockone.io dogedoge.com -donews.com -dongao.comdongguantoday.com dopa.com -douxie.com -downcc.com downxia.com -downyi.com -dpfile.comdreams-travel.comdrivergenius.com -dui.ai duiopen.com duoduocdn.com duokanbox.com -duomai.com duoshuo.com  duote.com dxpmedia.com -dxycdn.com  dyhjw.com dzsc.com dzw3.com  dzwww.com easesale.com  easiu.com eastday.com eastmoney.com eastsoo.com -eayyou.com  ebico.com  ebrun.com -ecitic.com ecjobhome.com -edu.cn  eduei.com eeyy.com eflycloud.com elecfans.com emarbox.com -emumax.com enterdesk.comeoeandroid.com excelhome.net expreview.comf.cx  faloo.com famulei.com  fanli.comfangxiaoer.com fanxian.com fastapi.net -feihuo.com feiniaomy.comfengkongcloud.com fengniao.comfengxian110.com  fenxi.com -fhldns.com  fimmu.com finchina.com fixdown.com  fqjob.net freebuf.com fugetech.com fuwj.com fxxz.com fyxz.com game234.com gameabc.com gamemei.com -gaodun.comgaoshouyou.comgaoxiaojob.com -gdjdxy.comgdliontech.com gdrc.com geilicdn.comgenelinkout.com  getui.com -gezida.com gfan.com -giocdn.com globrand.com gm86.com gmallbio.com  gmz88.comgobasearcher.com gold678.com  golue.comgongxiangcj.com goosail.com  goufw.com gree.comgreenxiazai.com growingio.com  gtags.net  guabu.com guaiguai.comguanaitong.comguanhaobio.com gucheng.com  guigu.org guoxinmac.com  gupzs.com gushiwen.orgguzhangting.com -gwdang.comgxairlines.com -gxb.io gzwanju.com h5uc.com hack520.com hackhome.com haishun.com hanboshi.com handanjob.comhangjiayun.com haolietou.com  haote.comhaoyouyinxiang.com hefei.cc -heisha.net  henha.com henkuai.com herostart.com  hexun.com  hiido.com hitv.com hiyd.com hktheone.com hn-cbec.com -hnrcsc.com hntv.tv hnyuyuhui.com hollycrm.com homeinns.com homekoo.comhongsanban.com -houdao.comhouyuantuan.com hteacher.net huanqiu.comhuashengdaili.com  huatu.com  huize.com  hujia.org hujiang.com hunantv.comhuochepiao.com huocheso.com  huxiu.com hxfy888.com hxsd.com iask.com -ibaotu.com ibuychem.com icaidao.com ichangtou.com idongdong.com iflytek.com  igao7.com -iheima.com ijiandao.com -ikafan.com im2maker.com  im-cc.com -imedao.com imgo.tv  imooc.com indienova.com  infoq.cominfoqstatic.com -ip-cdn.com  ip138.com ipinyou.com iranshao.com  irs01.com ishumei.com  it165.net  it168.com  it610.com  iteye.com -itjuzi.com  itmop.com  iyiou.com iyunxiao.com -izuche.com -jandan.net jb51.com  jdair.net jia.com jiameng.com jianbihua.com jianzhi8.com jiaodong.net jiaoyimao.com jiasuhui.com jiasule.com jiathis.com jiayuan.com jiazhao.com jiemeng8.comjikexueyuan.com jinianbi.comjinrongren.net  jinse.com  jinti.comjiqizhixin.com jisuanke.com jisuxia.com jiuzhilan.com jj20.com jkbl.com jmstatic.com job5588.com -job910.com  jobjm.com jsbc.com -jsrdgg.com jstv.com  jsyks.com juefeng.com -julive.com  jumei.com  juwan.com  juxia.com  jyimg.com k366.com kaifubiao.com kaixin001.com kameng98.com -kanimg.com kankanews.com kantianqi.net kanzhun.com kaopu001.comkeboyunxiao.com  kejet.net kf5.com -kimiss.com kingdee.com kkzj.com -kongfz.com koolearn.com -kouclo.com -koudai.com koudai8.com kq7.com  ksord.com  kuaaa.com kuaidi100.com kuaihou.com kuaizhan.com  kuqin.com -kuyiso.com lady8844.com laomaotao.net laomaotao.org  lascn.net  lawxp.com leanote.com ledu.comleetcode-cn.com leikeji.com leiphone.com leiting.com leju.com leturich.org lianmishu.com -liantu.com  liantu.me lianzhuli.comliaoxuefeng.com liba.com libaclub.comlieyunwang.com lightonus.com -likuso.com lingkou.com linkvans.com  linsn.com linuxidc.com liulanqi.net liuxiaoer.com live800.com liweijia.com lixinger.com lkkbrand.com longhoo.net -longre.comlongtugame.com longzhu.com  lotpc.com -loupan.com -lu.com luckyair.net  lufax.com lufaxcdn.com lunwengo.net luyouqi.com -lvmama.comlvsexitong.com  lxdns.com  lxway.com -magedu.com maisanqi.com -mamecn.com manhuatai.com manmankan.com -mapbar.com mayi.com -mayihr.com -mcbang.com  mdpda.com mechina.org -mediav.com meifang.com meika360.com meilishuo.commeimingteng.com -meiqia.commeishichina.com meishij.netmengtuoshi.wang mgtv.commiaobolive.com miaozhen.com -miercn.com mikecrm.comminhangshi.com miusi.co mjia.cc -mmbang.com mmbang.info -mofang.com mofazhu.com mogu.com mogucdn.com mogujie.com -mokahr.com moleecel.commoocollege.com mop.com msanjia.com -mscbsc.com mukewang.com -mumayi.com -muomou.com muzhiwan.com -mydown.com mydrivers.com myshow360.net myyx618.com myzaker.com naixuecha.com netbian.com -newasp.netnewbandeng.com  newyx.net nowcoder.com nowscore.com  nruan.com ntalker.com nvsheng.com  nzbdw.com oadz.com  oeeee.com  offcn.com officese.com -oicq88.com  okooo.com -ol-cdn.com -ol-img.com oldboyedu.com -oneapm.com onegreen.net -oneyac.comonlinedown.netonlinesjtu.com onlylady.com -opdown.com oray.com  ottcn.com -oupeng.com -p2peye.com p5w.netpaipaibang.com paopaoche.net pc6.com -pcbeta.com -pchome.net  pcpop.com  peccn.com pgzs.compharmacodia.com piaoliang.com -pingan.compingplusplus.com pingwest.com -pingxx.com pintu360.com  pipaw.com  pipaw.net pptbest.com psbc.com -pubyun.com  qc188.com qcc.com qcds.com qd8.com qd-metro.com qh24.com qiangbus.com qianjia.com qianlima.com qianzhan.comqiaobutang.com qichacha.com -qie.tv qifeiye.comqijiayoudao.net qingcloud.comqingdaochina.orgqingdaonews.comqingsongchou.com qiumibao.com qizuang.com  qqddc.comqqgexingqianming.com qqrizhi.comqqshuoshuo.com -qschou.com -qtmojo.com -qudong.com -qugame.com  qunar.com qunarzz.com qyer.com rfchost.comrockyenglish.com rong360.com rtbasia.com  ruan8.com -runoob.com rzrc114.com  saibo.com sanguosha.com -sanwen.net  scdng.com scoregg.com -sczsxx.com sdo.com sec-wiki.com -sencdn.com seniverse.comsenlinjimore.com -sgamer.com shang360.com -shangc.netshanghairc.com shangzhibo.tvshanshuihotel.com sharejs.com shebiaotm.comshelter-china.comshelterdome.netshengdaosoft.comshenmayouxi.com  sheui.com shijiexia.com shixiseng.com shmetro.comshoujihuifu.comshouyoubus.com shrbank.comshuidichou.comshuidihuzhu.com shumensy.comshuoshuokong.com -shutcm.com -simei8.com simuwang.com sinesafe.com  sjwyx.com  skycn.com sllssrq.com -snimay.com  sobot.com sofreight.com softwhy.com -sojson.com -somode.com soso.com souqian.comsouthmoney.comsparkletour.comspringtour.com ssports.com starbaby.cc starbaby.com stockstar.com sun0769.com -sunrtb.com supermap.comsupermapcloud.com -sys321.com szfw.org szhk.com szzfgjj.com t1y5.com  taihe.comtaihuoniao.com tangeche.com tanx.com -tao123.com -taoche.com te5.com tenxcloud.comtianjimedia.comtianjin-air.comtianlailive.com -tianqi.com tietuku.com -tiexue.net timedoo.com tingyun.com titanar.com tjjt360.com  tmcdn.net -tobosu.com tom.com tongdun.net topthink.com touqikan.com travelsky.com trunktech.comtrustexporter.com trustutn.org  tudou.com tudouui.com tuer123.com tuicool.com -tuiwen.net  tujia.com  tuniu.com tutuapp.com  tuwan.com tuwanjun.com u.tools  ucbug.com -uggame.com uoko.com urselect.comurumqi-air.com  useso.com uuu9.com uzzf.com  v5875.com value500.com vamaker.com vdolady.com veryhuo.com veryzhun.com  vic18.com videojj.com  vijos.org vip.com  vobao.com -vpgame.comwallstreetcn.comwangyangyang.vipwannianli8.com wanplus.com wanwushuo.com wanyiwang.com  wanyx.com -wdjimg.com wdzj.com  weand.com webterren.com weicaifu.com weidian.comweilaicaijing.comweilaiyunxiao.com weiphone.net weixing.com weiyangx.comwelltrend-edu.com wenjuan.comwhlovehome.com wicresoft.com -win007.com win8.netwindows10zj.comwindows7en.com windowszj.com winwin7.com wishdown.com  wmzhe.com wnhuifu.com wodingche.com  womai.com wonnder.com -wooyun.org worktile.comwoyaogexing.com  woyoo.com  wqiis.com wrating.com -wscdns.com  wtown.com  wumii.com -wuming.comwuximediaglobal.com  wywyx.com xafc.com  xbiao.com  xdwan.com -xfdown.com -xhscdn.comxiachufang.comxiaobaixitong.comxiaoe-tech.com xiaogouh5.comxiaohongshu.com  xiaoka.tv xiaokaxiu.comxiaomingtaiji.comxiaoxiongxitong.comxiaoyuxitong.com  xiayx.com xiazaiba.com xingjiesj.com xinhuanet.comxinrenxinshi.com xinshipu.comxingtan001.com xitong8.com xitu.com xiu8.com -xiucai.com xiziwang.net xmhouse.com  xnpic.com  xpgod.com -xsa239.comxuanchuanyi.com  xue51.com xuekanba.com xuexicha.com xundupdf.com  xycdn.com xywy.com -xzking.com yahui.cc -yaolan.com  yaoyl.com  yaozh.com -ycgame.com -yeepay.com -yeshen.com  yesky.com yh31.com  yicai.com  yigao.com yigoonet.com yihedoors.comyikaochacha.com yikexue.comyingjia360.comyinhang123.net yinxiang.com yinyuetai.comyinzhaowang.com -yiqifa.com -yiqifa.org yisu.com yitaifang.com  yiwan.com yixi.tv yizhibo.com  yjbys.com ynhr.com -yocajr.com -youboy.com youjiao.com youke.co youlian.fun youshang.com youxiake.com youxihezi.net youxila.com youxiniao.com youxigt.com  youzu.com yoyojie.com  yoyou.com  ypppt.com -yslyhr.com  ysten.com -yuewen.com  yunaq.com yunba.io yundaex.com yundasys.com -yundun.com yunqishi.net yunshipei.com yunxiao.com yunzhijia.com  yupoo.com  yuzua.com  yxbao.com -yxdown.com -yytcdn.com -zampda.net zampdsp.com zastatic.com -zbjimg.com zczj.com -zdfans.com zgjm.org -zgjsks.com  zglxw.com  zgzcw.com zhanbuba.comzhangyoubao.com  zhanqi.tv zhanzhang.net zhaodanji.com zhaopin.com zhcw.com -zhenai.com  zhibo8.cc  zhidx.com zhifang.com -zhipin.com zhiziyun.com zhongkao.comzhuangjiba.comzhuanyewanjia.comzhuayoukong.com zhubajie.com zhugeapi.net zhugeio.com  zimuzu.io zixiaomao.com zixuntop.com  zjstv.com zmengzhu.com zongheng.com -zpb365.com -zrblog.net zsxq.com  zuche.com zuchecdn.com zuhaowan.com zuidaima.com zuowen8.comzuowenwang.net jindidata.comtianyancha.comzhuanxiandai.com -fm3838.comlarsonlimited.com runsheng.com vzuu.com -zoom3g.com -zoomyou.cn chaoxing.com chaoxing.cc - - -GLOBALSIGNglobalsign-media.com globalsign.be globalsign.chglobalsign.co.ukglobalsign.comglobalsign.com.auglobalsign.com.hkglobalsign.com.sg globalsign.es globalsign.eu globalsign.frglobalsign.net globalsign.nl!secure.globalsign.com -cnocsp.globalsign.com -cn ocsp2.globalsign.com -cn - -TSQUARE -tsquare.tv - -DW -dw.com - -IBMibmweatherchannel weather bluemix.com bluemix.net ibm.com -ibm.eu ibm.net -ibm.us s-bluemix.net s81c.com -+ -MYFONTS myfonts.com myfonts.net - -MOVEFREEkeepmovingwithmovefree.com move-free.net movefree.cn -cn movefrees.commovefreeoffers.commovefreerewards.comschiffvitamins.com -5 -SENSORSDATA-ADS"static.sensorsdata.cn -ads - -COINBASE coinbase.com - - -EVERBRIGHT cebbank.com ceblease.com -eb-ind.com ebasset.com ebchina.comebchinaintl.com -ebf.com.cn  ebfcn.com ebfortune.com ebtrust.com ebscn-am.com  ebscn.comeverbright.com  secec.comsunlife-everbright.com -$ - FREECODECAMPfreecodecamp.org - -HKCNEWS hkcnews.com -, - -HOTJAR-ADSstatic.hotjar.com -ads -w -JWPLAYER -jwpcdn.comjwplatform.com jwplayer.com -jwpltx.com -jwpsrv.comlongtailvideo.com - -CATEGORY-BANK-CN -boc.cnbankofchina.combocaviation.com  boccfc.cn  bocgi.com bocgins.com  bochk.com bocichina.com bocigroup.com  bocim.combocservices.com.cn cignacmb.comcmb-leasing.com cmbchina.com cmbi.com.hkcmbwinglungbank.comicbc icbc-ltd.com icbc-us.com icbc.com.cn  95516.com unionpay.comunionpayintl.comunionpaysecure.com abchina.com  whccb.com - -CATEGORY-MOOC-CNchinesemooc.org -cnmooc.orgicourse163.org icourses.cn mooc.cnopenlearning.com pmphmooc.commoocs.unipus.cnopencourse.pku.edu.cn study.163.com -c -COMODO -comodo.com comodoca.com comodoca2.com comodoca3.com comodoca4.com - -XART  x-art.com -% -IMGIX  imgix.com  imgix.net - -4399 3304399.net 4399.com 4399api.com 4399dmw.com -4399er.com4399youpai.com 5054399.com img4399.com -' -APA apa.orgpsyccareers.com - -FIREFOX firefox.com -j -IMPERIALCOLLEGE ic.ac.ukimperial.ac.ukimperialbusiness.schoolimperial.insendi.com -P - KUAISHOU-ADS log-sdk.gifshow.com -adswlog.kuaishou.com -ads - -MARVEL -marvel.commarvel10thanniversary.commarveldimensionofheroes.commarvelparty.netmarvelpinball.commarvelsdoubleagent.commarvelspotlightplays.com marvelsuperheroseptember.commarvelsuperwar.comthemarvelexperiencetour.com -- -MYRADIO -myradio.hkmyradio.com.hk -} -SHOPIFY myshopify.com shopify.com shopify.devshopifycdn.comshopifystatus.comshopifysvc.com -$ - BROWSERLEAKSbrowserleaks.com -8 -DMM dmm.com  dmm.co.jpdmm-extension.com - -DOOM9  doom9.org -( -UDEMY  udemy.com udemycdn.com - -NODEJS -nodejs.org - -SSPLIVE -ssplive.pw - -WOOLITE -woolite.ca -woolite.cn -cn woolite.comwoolite.com.cn -cn -woolite.pl -woolite.uswoolitecarpet.com -$ - LOCALPRESSHKlocalpresshk.com -, - BYTEDANCE-ADSmcs.snssdk.com -ads -H -COOLAPK coolapk.comcoolapkmarket.comcoolapkmarket.net - -DOI doi.org doi.info - -THESUNdreamteamfc.com  page3.comsunbingo.co.uk talksport.comthescottishsun.co.uk thesun.co.uk  thesun.ie -) -EASYLIST easylist.to lanik.us - -FINISH$dishwashingexpert.com.cn -cn  finish.at  finish.bg finish.co.kr finish.co.nz finish.co.uk finish.co.za finish.com.cn -cn finish.com.hr finish.com.tr  finish.de  finish.es  finish.fr  finish.gr  finish.hu  finish.lv  finish.pl  finish.pt  finish.ro  finish.si  finish.skfinisharabia.comfinishbrasil.com.brfinishdishwashing.cafinishdishwashing.com finishinfo.be finishinfo.clfinishinfo.comfinishinfo.com.arfinishinfo.com.au finishinfo.cz finishinfo.fi finishinfo.it finishinfo.jp finishinfo.nl finishinfo.no finishinfo.ru finishinfo.sefinishkilpailu.fi finishwin.beyarininsuyu.com - -SHOPEE shopee.cn -cn shopee.co.id shopee.co.th -shopee.com shopee.com.br shopee.com.my  shopee.ph  shopee.sg  shopee.tw  shopee.vnshopeemobile.com shopeesz.com -cn -shp.ee - -CATEGORY-ADS-ALLad-delivery.net -ads adinplay.com -ads adnxs.com -adsadvertserve.com -adscasalemedia.com -ads!contextual.media.net -ads cpmstar.com -ads -demdex.net -ads httpool.com -ads lijit.com -ads1rx.io -ads mfadsrvr.com -adsmgid.com -adsns1p.net -ads openx.net -ads pubmatic.com -adsspotxchange.com -ads -unimhk.com -ads upapi.net -ads taboola.com apilog-web.acfun.cn -ads adcolony.com -ads -adjust.com -ads a.alimama.cn -adsacjs.aliyun.com -adsadash.m.taobao.com -ads!adashbc.m.taobao.com -ads"adashbc.ut.taobao.com -ads"adash-c.ut.taobao.com -ads adashx.m.taobao.com -ads$adashx4ae.ut.taobao.com -ads#adashx4yt.m.taobao.com -ads#adashxgc.ut.taobao.com -adsadsh.m.taobao.com -adsafp.adchina.com -adsafp.alicdn.com -ads agoodm.m.taobao.com -ads#agoodm.wapa.taobao.com -ads alipaylog.com -adsamdc.alipay.com -adsamdc.m.taobao.com -ads api.wapa.taobao.com -ads#api.waptest.taobao.com -adsapoll.m.taobao.com -ads#appdownload.alicdn.com -adsatanx.alicdn.com -adsatanx2.alicdn.com -adscbu01.alicdn.com -adscdn0.mobmore.com -adsclick.aliyun.com -ads&click.mz.simba.taobao.com -adscm.ipinyou.com -ads cm.mlt01.com -ads!dsp.simba.taobao.com -adserr.taobao.com -adsex.mobmore.com -ads ex.puata.info -ads!fav.simba.taobao.com -ads$feedback.whalecloud.com -adsff.win.taobao.com -ads fm.p0y.cn -adsg.click.taobao.com -ads -g.tbcdn.cn -adsgma.alicdn.com -adsgtms01.alicdn.com -adsgtms02.alicdn.com -adsgtms03.alicdn.com -adsgtms04.alicdn.com -adshydra.alibaba.com -ads'hz.pre.tbusergw.taobao.net -ads#hz.tbusergw.taobao.net -ads i.ipinyou.com -adsinit.phpwind.com -ads!intl.wapa.taobao.com -ads$intl.waptest.taobao.com -ads"jxlog.istreamsche.com -adslog.umtrack.com -adsm.intl.taobao.com -adsm.simba.taobao.com -adsmatch.p4p.1688.com -ads%material.istreamsche.com -ads mlt01.com -ads&nbsdk-baichuan.alicdn.com -ads&nbsdk-baichuan.taobao.com -ads"osfota.cdn.aliyun.com -ads osupdate.aliyun.com -ads&osupdateservice.yunos.com -ads%pindao.huoban.taobao.com -adspush.wandoujia.com -adsre.m.taobao.com -ads re.taobao.com -ads&redirect.simba.taobao.com -adsrj.m.taobao.co -adsrj.m.taobao.com -adssdkinit.taobao.com -adsshow.re.taobao.com -adssimaba.taobao.com -adssimba.m.taobao.com -ads!srd.simba.taobao.com -adsstats.ipinyou.com -ads strip.taobaocdn.com -ads$tanxlog.istreamsche.com -adstejia.taobao.com -adstemai.taobao.com -ads!tns.simba.taobao.com -ads tongji.linezing.com -ads tvupgrade.yunos.com -adstyh.taobao.com -adsuserimg.qunar.com -adsw.m.taobao.com -adsyiliao.hupan.com -ads amazon-adsystem.com -adsiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -ads applovin.com -ads -applvn.com -ads atom-data.io -adsanalytics-data.io -ads ironbeast.io -ads a.baidu.com -ads ad.duapps.com -ads ad.player.baidu.com -ads adm.baidu.com -adsadscdn.baidu.com -adsadx.xiaodutv.com -adsae.bdstatic.com -ads afd.baidu.com -ads als.baidu.com -ads&api.mobula.sdk.duapps.com -ads"api.tuisong.baidu.com -adsappc.baidu.com -adsb.bdstatic.com -adsbaichuan.baidu.com -adsbaidustatic.com -adsbaidutv.baidu.com -adsbanlv.baidu.com -ads bar.baidu.com -ads"bdimg.share.baidu.com -adsbdplus.baidu.com -ads bj.bcebos.com -adsbtlaunch.baidu.com -ads c.baidu.com -ads cb.baidu.com -adscbjs.baidu.com -adscjhq.baidu.com -adscleaner.baidu.com -ads click.bes.baidu.com -adsclick.hm.baidu.com -adsclick.qianqian.com -ads"cm.baichuan.baidu.com -ads cm.baidu.com -adscm.pos.baidu.com -ads cpro.baidu.cn -adscpro.baidu.com -ads!cpro.baidustatic.com -ads!cpro.tieba.baidu.com -ads"cpro.zhidao.baidu.com -adscpro2.baidu.com -ads"cpro2.baidustatic.com -ads crs.baidu.com -adsdatax.baidu.com -ads dd713.bj.bcebos.com -ads dl.client.baidu.com -adsdl.ops.baidu.com -adsdl1sw.baidu.com -adsdl2.bav.baidu.com -adsdlsw.baidu.com -adsdlsw.br.baidu.com -ads!dl-vip.bav.baidu.com -ads(dl-vip.pcfaster.baidu.co.th -ads#download.bav.baidu.com -ads"download.sd.baidu.com -adsdrmcmm.baidu.com -ads dup.baidustatic.com -ads dxp.baidu.com -ads dzl.baidu.com -ads e.baidu.com -adseclick.baidu.com -adsecma.bdimg.com -adsecmb.bdimg.com -adsecmc.bdimg.com -ads eiv.baidu.com -ads em.baidu.com -adsentry.baidu.com -ads ers.baidu.com -ads f10.baidu.com -ads#fc-feed.cdn.bcebos.com -adsfclick.baidu.com -adsfeed.baidu.com -adsfexclick.baidu.com -ads g.baidu.com -adsgimg.baidu.com -adsguanjia.baidu.com -ads hc.baidu.com -ads hm.baidu.com -adshmma.baidu.com -ads hpd.baidu.com -adsidm.bce.baidu.com -adsidm-su.baidu.com -adsiebar.baidu.com -adsikcode.baidu.com -adsimg.taotaosou.cn -adsimg01.taotaosou.cn -adsj.br.baidu.com -adskstj.baidu.com -ads log.music.baidu.com -ads m1.baidu.com -ads ma.baidu.com -adsmobads.baidu.com -ads"mobads-logs.baidu.com -adsmpro.baidu.com -adsmsite.baidu.com -adsneirong.baidu.com -adsnsclick.baidu.com -ads#nsclickvideo.baidu.com -adsopenrcv.baidu.com -ads$pc.videoclick.baidu.com -ads pos.baidu.com -adspups.bdimg.com -ads%push.zhanzhang.baidu.com -ads qianclick.baidu.com -adsrelease.baidu.com -adsres.mi.baidu.com -ads"rigel.baidustatic.com -ads#river.zhidao.baidu.com -ads rj.baidu.com -adsrplog.baidu.com -ads s.baidu.com -adss.cpro.baidu.com -ads!sa.tuisong.baidu.com -adssclick.baidu.com -adssestat.baidu.com -adsshadu.baidu.com -adsshare.baidu.com -ads"snippet.pos.baidu.com -adssobar.baidu.com -adssobartop.baidu.com -adsspcode.baidu.com -adsstat.v.baidu.com -ads su.bdimg.com -adssu.bdstatic.com -ads t10.baidu.com -ads t11.baidu.com -ads t12.baidu.com -ads tk.baidu.com -adstkweb.baidu.com -ads"tob-cms.bj.bcebos.com -adstongji.baidu.com -adstoolbar.baidu.com -adstracker.baidu.com -adstuijian.baidu.com -ads*uat1.bfsspadserver.8le8le.com -ads#ubmcmm.baidustatic.com -adsucstat.baidu.com -adsulic.baidu.com -ads ulog.imap.baidu.com -adsunion.baidu.com -ads!unionimage.baidu.com -adsutility.baidu.com -ads utk.baidu.com -ads videopush.baidu.com -adsvv84.bj.bcebos.com -adsw.gdown.baidu.com -ads w.x.baidu.com -adswangmeng.baidu.com -adsweishi.baidu.com -ads$wenku-cms.bj.bcebos.com -ads%wisepush.video.baidu.com -ads wm.baidu.com -adswn.pos.baidu.com -adsznsv.baidu.com -adszz.bdstatic.com -adszzy1.quyaoya.com -adsmcs.snssdk.com -adsx.clearbitjs.com -ads bluekai.com -adsfout.jp -ads gmossp-sp.jp -adsi-mobile.co.jp -ads im-apps.net -ads impact-ad.jp -ads ladsp.com -ads microad.co.jp -ads -microad.jp -ads socdm.com -ads emogi.com -adsdata.flurry.com -ads2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -ads!assets.growingio.com -adsmlog.hiido.com -adsylog.hiido.com -adsstatic.hotjar.com -adsclick.hunantv.com -adsda.hunantv.com -ads da.mgtv.com -adslog.hunantv.com -adslog.v2.hunantv.com -adsres.hunantv.com -adsv2.log.hunantv.com -adsinner-active.mobi -adsa.ckm.iqiyi.com -adsad.m.iqiyi.com -ads afp.iqiyi.com -adsc.uaa.iqiyi.com -ads cloudpush.iqiyi.com -ads"cm.passport.iqiyi.com -adscupid.iqiyi.com -ads#emoticon.sns.iqiyi.com -ads!gamecenter.iqiyi.com -ads!hotchat-im.iqiyi.com -adsifacelog.iqiyi.com -adsmbdlog.iqiyi.com -ads msg.71.am -adsmsg.video.qiyi.com -ads msg2.video.qiyi.com -ads#policy.video.iqiyi.com -adsyuedu.iqiyi.com -adsads.union.jd.com -ads c-nfa.jd.com -adscps.360buy.com -ads -jzt.jd.com -ads stat.m.jd.com -ads img-x.jd.com -ads log-sdk.gifshow.com -adswlog.kuaishou.com -ads leanplum.com -ads#api.game.letvstore.com -ads ark.letv.com -ads dc.letv.com -adsdev.dc.letv.com -ads fz.letv.com -ads g3.letv.com -adsletv.allyes.com -adsminisite.letv.com -adsmsg.m.letv.com -adsn.mark.letv.com -adsplog.dc.letv.com -adspro.hoye.letv.com -ads pro.letv.com -ads stat.letv.com -ads"static.app.m.letv.com -ads decide.mixpanel.com -ads mopub.com -adsevt.mxplay.com -adsanalytics.163.com -ads crash.163.com -ads crashlytics.163.com -ads g.163.com -ads"js-agent.newrelic.com -ads ogury.com -ads -presage.io -adsogury.co -ads onesignal.com -adsOB^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$ -ads cdn.pocoiq.cn -ads oct.pocoiq.cn -adsads.pubmatic.com -adscdn.segment.com -ads"static.sensorsdata.cn -ads 2016.sina.cn -adsad.sina.com.cn -adsadbox.sina.com.cn -ads!adimages.sina.com.hk -ads!adimg.mobile.sina.cn -ads adimg.uve.weibo.com -ads!adm.leju.sina.com.cn -ads ads.sina.com -adsalitui.weibo.com -adsapi.apps.sina.cn -ads appgift.sinaapp.com -ads#appsupdate.sinaapp.com -ads atm.sina.com -adsbeacon.sina.com.cn -ads biz.weibo.com -adsc.biz.weibo.com -ads!c.wcpt.biz.weibo.com -ads%click.uve.mobile.sina.cn -ads click.uve.weibo.com -adscm.dmp.sina.cn -ads0#contentrecommend-out.mobile.sina.cn -ads$count.video.sina.com.cn -ads counter.sina.com.cn -adscre.dp.sina.cn -adscre-dp.sina.cn -adsd00.sina.com.cn -adsdcads.sina.com.cn -adsdl.kjava.sina.cn -ads dmp.sina.cn -adsdp.im.weibo.cn -ads game.weibo.cn -adsgame.weibo.com -ads"gw5.push.mcp.weibo.cn -ads"gw6.push.mcp.weibo.cn -ads"img.adbox.sina.com.cn -ads#img.amp.ad.sina.com.cn -ads"int.dpool.sina.com.cn -ads%interest.mix.sina.com.cn -ads)log.interest.mix.sina.com.cn -ads log.mix.sina.com.cn -ads log.sina.cn -ads!m.beacon.sina.com.cn -adsm.game.weibo.cn -ads!mg.games.sina.com.cn -ads!motu.p4p.sina.com.cn -adsnewspush.sinajs.cn -ads oascentral.sina.com -ads#oascentral.sina.com.hk -adsp4p.sina.com.cn -adspalmnews.sina.cn -adspay.mobile.sina.cn -adspfp.sina.com.cn -adspfpip.sina.com -ads!promote.biz.weibo.cn -ads r.dmp.sina.cn -adsrm.sina.com.cn -ads rs.sinajs.cn -adss.alitui.weibo.com -ads"s.img.mix.sina.com.cn -ads sapi.sina.cn -ads sax.sina.cn -adssax.sina.com.cn -adssaxn.sina.com.cn -adssaxs.sina.com.cn -ads sbeacon.sina.com.cn -ads"sdkapp.mobile.sina.cn -ads!sdkapp.uve.weibo.com -ads$sdkclick.mobile.sina.cn -ads slog.sina.cn -adsslog.sina.com.cn -ads'staticadm.leju.sina.com.cn -adstjs.sjs.sinajs.cn -ads"trends.mobile.sina.cn -ads"u1.img.mobile.sina.cn -ads&wapwbclick.mobile.sina.cn -ads!wbapp.mobile.sina.cn -ads wbapp.uve.weibo.com -ads#wbclick.mobile.sina.cn -ads$wbpctips.mobile.sina.cn -adswidget.weibo.com -ads woocall.sina.com.cn -adszc.biz.weibo.com -adszymo.mps.weibo.com -adsadnet.sohu.com -ads ads.sohu.com -ads!adv-sv-show.focus.cn -ads aty.sohu.com -adsdata.vod.itc.cn -adsepro.sogou.com -ads go.sohu.com -adsgolden1.sogou.com -adsimp.optaim.com -adsinte.sogou.com -adsinte.sogoucdn.com -adslu.sogoucdn.com -ads pv.sohu.com -adstheta.sogoucdn.com -adstrack.sohu.com -adsuranus.sogou.com -adsv2.reachmax.cn -ads)e8aeb8bbdbbd7.cdn.sohucs.com -ads)643108e7617ef.cdn.sohucs.com -adssupersonic.com -adssupersonicads.com -ads -ssacdn.com -ads)supersonicads-a.akamaihd.net -ads g1.tagtic.cn -adsxy-log.tagtic.cn -adsssp.api.tappx.com -ads ad.qq.com -ads ad.qun.qq.com -ads!adfilter.imtt.qq.com -ads adping.qq.com -adsadpm.app.qq.com -adsadsclick.qq.com -adsadsfile.qq.com -adsadshmmsg.qq.com -adsadslvfile.qq.com -adsadslvseed.qq.com -ads%adsmind.apdcdn.tc.qq.com -adsadsqqclick.qq.com -adsadstestview.qq.com -adsadsview.qq.com -adsadsview2.qq.com -adsadv.app.qq.com -ads adver.qq.com -ads btrace.qq.com -adse.qq.com -adslog.tbs.qq.com -ads mpush.qq.com -ads mtrace.qq.com -ads push.qq.com -ads pgdt.gtimg.cn -ads uberads.com -adsalog.umeng.com -ads alog.umengcloud.com -ads -w.cnzz.com -ads!unityads.unity3d.com -ads#unityads.unitychina.cn -ads ads.wteam.xyz -ads#collector.xhamster.com -adsad.intl.xiaomi.com -ads ad.mi.com -ads ad.xiaomi.com -adsadmob.xiaomi.com -adsad1.xiaomi.com -adsmistat.xiaomi.com -adstracking.miui.com -adsadv.sec.miui.com -ads"adv.sec.intl.miui.com -ads"misc.in.duokanbox.com -ads ad.hpplay.cn -adsadeng.hpplay.cn -ads atianqi.com -ads kuyun.com -ads umeng.com -ads ads.yahoo.com -adsgemini.yahoo.com -ads ysm.yahoo.com -adsactives.youku.com -ads ad.api.3g.youku.com -ads$ad.api.mobile.youku.com -ads ad.mobile.youku.com -ads(a-dxk.play.api.3g.youku.com -ads atm.youku.com -ads#b.smartvideo.youku.com -adsc.yes.youku.com -adsdas.api.youku.com -ads!das.mobile.youku.com -ads!dev-push.m.youku.com -adsdl.g.youku.com -adsdmapp.youku.com -adse.stat.ykimg.com -ads#gamex.mobile.youku.com -ads!guanggaoad.youku.com -ads hudong.pl.youku.com -ads!huodong.pl.youku.com -ads"huodong.vip.youku.com -ads hz.youku.com -adsiyes.youku.com -ads l.ykimg.com -adslstat.youku.com -ads mobilemsg.youku.com -ads msg.youku.com -adsmyes.youku.com -ads p.l.youku.com -ads pl.youku.com -ads#passport-log.youku.com -adsp-log.ykimg.com -adspush.m.youku.com -ads r.l.youku.com -ads s.p.youku.com -adssdk.m.youku.com -adsstat.youku.com -ads$statis.api.3g.youku.com -ads&store.tv.api.3g.youku.com -ads&store.xl.api.3g.youku.com -adstdrec.youku.com -adstest.ott.youku.com -ads!test.sdk.m.youku.com -ads v.l.youku.com -adsval.api.youku.com -ads wan.youku.com -adsykatr.youku.com -adsykrec.youku.com -ads cvnad.com -ads -iyoowi.com -ads zynga.com -ads#zyngaplayersupport.com -ads 51y5.net a.adtng.com -aaxads.com -ad-srv.netad.api.moji.comad.wang502.com adbutter.netads.trafficjunky.net adtechus.comcfts1tifqr.comcontentabc.comimg-bss.csdn.net imglnkc.com imglnkd.comja2.gamersky.com jl3.yjaxa.topkepler-37b.com -lqc006.coms4yxaqyq95.comshhs-ydd8x2.yjrmss.cnstatic.javhd.comtrafficfactory.biz adx.36kr.comclick.ali213.netpbmp.ali213.net^pinggai\d\.caixin\.com$adq.chinaso.comstat.chinaso.com toboads.comdn-growing.qbox.me ad.unimhk.comdas-rpt-log.ucloud.cn adxprtz.com exoclick.com -exosrv.comcdn.advertserve.comcdn.banclip.comcdn.tsyndicate.com -% -FONTS  fonts.com  fonts.net - -FREENODE freenode.net -1 -JINSHUJU jinshuju.netjinshujucdn.com - - MACRUMORS macrumors.com -# -EDX edx.org edx-cdn.org - - INSTAGRAM!achat-followers-instagram.com#acheter-followers-instagram.com$ acheterdesfollowersinstagram.com!acheterfollowersinstagram.combookstagram.comcarstagram.comcdninstagram.comchickstagram.com ig.me  igcdn.com igsonar.com igtv.com imstagram.com imtagram.cominstaadder.cominstachecker.cominstafallow.cominstafollower.cominstagainer.com instagda.com instagify.cominstagmania.com instagor.cominstagram-brand.cominstagram-engineering.cominstagram-help.cominstagram-press.cominstagram-press.net instagram.cominstagramci.cominstagramcn.cominstagramdi.cominstagramhashtags.netinstagramhilecim.cominstagramhilesi.orginstagramium.cominstagramizlenme.cominstagramkusu.cominstagramlogin.cominstagramm.cominstagramn.cominstagrampartners.cominstagramphoto.cominstagramq.cominstagramsepeti.cominstagramtakipcisatinal.netinstagramtakiphilesi.cominstagramtips.cominstagramtr.com instagran.cominstagranm.com instagrem.com instagrm.cominstagtram.com instagy.cominstamgram.cominstangram.cominstanttelegram.cominstaplayer.net instastyle.tv instgram.com intagram.com intagrm.com intgram.comkingstagram.comlnstagram-help.comtheinstagramhack.comoninstagram.comonline-instagram.comonlineinstagram.comweb-instagram.netwwwinstagram.com - - JFENGTIME jfengtime.com -C -MAOCLOUD -maoyun.commaoyuncloud.cnmaoyuncloud.com -' -ZEPLIN -zeplin.dev  zeplin.io - -ARK -ark.to - -HBO cinemax.comforthethrone.com hbo.com hboasia.com  hbogo.com hbogoasia.com hbogoasia.hk -hbomax.com -hbonow.com  maxgo.com -6 -75TEAM 75.team -75team.com baomitu.com - -KALI kali.org - - SHARETHIS sharethis.com -9 -DOUBAN -douban.com  douban.fm doubanio.com -d -REDDIT -reddit.comredditstatic.comredditmail.comredditmedia.com redd.it - -VMWARE -cfblob.comcloudchoose.comcloudvolumes.com -cmpaas.com -cpedge.comcrit-staging.comexperience-vmware.com gemfire.netgeodesummit.com getboxer.com gitstar.com gitstar.net gopivotal.net greenplum.net hwslabs.com immidio.comitfromtheinside.comlatamvmwareforum.compivotalinitiative.comsnapvolumes.com ssdevrd.com vcanedge.com vcloudair.net vfabric.netvidmpreview.com -virsto.com -virsto.net vmglobal.net -vmmark.comvmtestdrive.com vmw.comvmware-cloudmanagement.comvmware-techcenter.comvmware20mosaic.comvmwareausnews.com!vmwarecertificationvideos.comvmwarecloud.comvmwaredemandcenter.comvmwareemeablog.comvmwaregrid.comvmwarehorizon.comvmwareidentity.comvmwarelearning.comvmwarelearningplatform.comvmwarestuff.comvmwareusergroupstore.comvmwareviewpoint.com vmwarevmc.com vmwdemo.com vmwgcomms.comvmwhorizonair.comvmwidm-ads.com -vmwidm.comvmwlabconnect.comvmwlearningplatform.com vmworld.comvmworld2010.comvmworld2013.comvmwsalesrewards.comvmwservices.com -vnware.netwhychoosehorizon.comwhychooseview.comwhychoosevmwareeuc.comwindowsuem.comworkspaceair.com xlstudio.comavinetworks.com -badaas.com bitnami.combitnamiapp.combitnamistudio.comdockerizer.comcloudcredibility.comcloudhealthtech.com cloudlive.comkubeacademy.com kubeapps.com -nyansa.com velocloud.com gopivotal.com heptio.cn -cn -heptio.com aliveipc.com aliveitsm.comaliveprofiler.com alivercm.comaliverewind.com alivertsm.com alivevm.com alivevmax.com alivevue.com vsphere.com vsphere.net air-watch.comairwatchexpress.comairwatchqa.com -getws1.com getwsone.comwhatisairwatch.comwhatisworkspaceone.comworkspaceone.comvmware.tt.omtrdc.net -u -CREATIVECOMMONS ccstatic.orgcreativecommons.orgcreativecommons.engineeringlicensebuttons.net - - -LANDOFHOPE landofhope.tv -4 -PERL perl.org cpan.org metacpan.org -w -NYTIMES nyt.com nyt.net nytcn.me  nytco.com nytimes.com nytstyle.comtimestalks.com -6 -PROQUESTserialssolutions.com proquest.com -s -BMWmini4myminicard.comaboutyourmini.comatlantaminidealers.comgreatergothammini.com!midatlanticbmwmotorcycles.commini-abudhabi.commini-antilles.frmini-bahrain.commini-bosnia.com mini-clubs-international.commini-connected.atmini-connected.bemini-connected.chmini-connected.co.ukmini-connected.commini-connected.czmini-connected.demini-connected.dkmini-connected.eemini-connected.esmini-connected.fimini-connected.frmini-connected.iemini-connected.itmini-connected.ltmini-connected.lumini-connected.lvmini-connected.nlmini-connected.nomini-connected.plmini-connected.ptmini-connected.semini-corporate-sales.com mini-coupe.camini-dubai.com -mini-e.commini-egypt.commini-georgia.commini-grouparchiv.demini-grouparchive.commini-jordan.com mini-ksa.commini-kuwait.commini-lebanon.com mini-me.com mini-oman.commini-qatar.commini-srilanka.commini-stjohns.camini-stjohns.commini-tahiti.commini-vietnam.commini-windsor.camini-windsor.com mini.am mini.at mini.be mini.bg mini.by mini.ca mini.cc mini.ch mini.cl -mini.co.cr -mini.co.id -mini.co.il -mini.co.kr -mini.co.me -mini.co.th -mini.co.uk -mini.co.za mini.com mini.com.ar mini.com.bn mini.com.br mini.com.co mini.com.cy mini.com.do mini.com.ec mini.com.gr mini.com.gt mini.com.hr mini.com.mk mini.com.mo mini.com.mt mini.com.mx mini.com.pa mini.com.pe mini.com.ph mini.com.pl mini.com.py mini.com.sg mini.com.tr mini.com.tw mini.com.uy mini.cz mini.de mini.dk mini.dz mini.es mini.fi mini.fr mini.gp mini.hu mini.ie mini.in mini.is mini.it mini.jp mini.kz mini.lu mini.ma mini.md mini.mq mini.mu mini.my mini.nc mini.nl mini.no mini.pt mini.re mini.ro mini.rs mini.ru mini.se mini.si mini.sk mini.tm mini.tn mini.ua -mini101.caminiargentina.comminibrossard.caminibrossard.com minicanada.caminicaribbean.com miniccrc.ca minicooper.caminidealer.comminidealernet.comminidowntown.caminidowntown.com minidrivingexperienceusa.com minidurham.caminidurham.com  minie.com -minifs.comminigeorgian.caminigrandriver.comminihalifax.caminihalifax.com -minihk.comminiinvasion.caminiitalianjob.comminikelowna.caminikelowna.comminilangley.caminilangley.com minilat.com minilaval.ca minilaval.com minilondon.ca minilondon.cominimarkham.caminimarkham.comminimoncton.caminimoncton.comminimontrealcentre.caminimontroyal.caminimontroyal.comminimotoringrewards.comminimotoringschool.comminimotorsport.commininanaimo.camininanaimo.comminioakville.comminiofmonrovia.comminipetfriendly.comminirichmond.caminirichmond.comminisaskatoon.caminisaskatoon.com minishop.ca minispace.comminispecialoffers.caminispygear.comministcatharines.caministeagathe.comministjohns.caminitakesthestates.comminitoronto.caminitroisrivieres.ca miniusa.comminiusaservice.comminiusatires.comminivalueservice.comminivancouver.caminivaughanwest.caminivaughanwest.comminivictoria.caminivictoria.comminivilledequebec.caminivilledequebec.comminiwholesaleconnect.com miniwidget.caminiwindsor.comminiworkshop.comminiyaletown.capugetsoundmini.comshopminiusa.comsouthfloridamini.com -trmini.comtroisrivieresmini.comyours-customised.mini.combmwapprovedusedbmw.combaltimorebmw.combayareabmw.combmw-abudhabi.combmw-adventskalender.combmw-albania.combmw-antilles.frbmw-art-journey.combmw-arts-design.com bmw-asia.com bmw-auslieferungszentrum.combmw-authority-vehicles.combmw-bahrain.combmw-businessdrive.combmw-calgary.ca bmw-carit.debmw-classic.combmw-clubs-international.combmw-connected-drive.combmw-connecteddrive.aebmw-connecteddrive.atbmw-connecteddrive.bebmw-connecteddrive.bgbmw-connecteddrive.cabmw-connecteddrive.chbmw-connecteddrive.co.nzbmw-connecteddrive.co.ukbmw-connecteddrive.co.zabmw-connecteddrive.combmw-connecteddrive.com.aubmw-connecteddrive.com.brbmw-connecteddrive.com.cybmw-connecteddrive.com.hrbmw-connecteddrive.com.kwbmw-connecteddrive.com.mtbmw-connecteddrive.czbmw-connecteddrive.debmw-connecteddrive.dkbmw-connecteddrive.eebmw-connecteddrive.esbmw-connecteddrive.fibmw-connecteddrive.frbmw-connecteddrive.grbmw-connecteddrive.hubmw-connecteddrive.iebmw-connecteddrive.itbmw-connecteddrive.jpbmw-connecteddrive.krbmw-connecteddrive.ltbmw-connecteddrive.lubmw-connecteddrive.lvbmw-connecteddrive.mxbmw-connecteddrive.mybmw-connecteddrive.nlbmw-connecteddrive.nobmw-connecteddrive.plbmw-connecteddrive.ptbmw-connecteddrive.robmw-connecteddrive.rubmw-connecteddrive.sebmw-connecteddrive.sgbmw-connecteddrive.sibmw-connecteddrive.skbmw-connecteddrive.twbmw-corporate-sales.combmw-diplomatic-sales.combmw-driving-center.co.krbmw-drivingexperience.com bmw-dubai.com -bmw-eg.com bmw-fleet.net bmw-ghana.combmw-golfsport.com bmw-group.com bmw-group.netbmw-grouparchiv.debmw-grouparchives.com -bmw-gta.cabmw-i-pure-impulse.com bmw-i.jp bmw-int1.com bmw-iraq.combmw-jordan.combmw-konzernarchiv.de bmw-ksa.combmw-kuwait.com -bmw-lao.labmw-lebanon.combmw-m-safetycar.com  bmw-m.combmw-mdrivetour.com -bmw-me.combmw-military-sales.combmw-motorcycle.combmw-motorcycles.vnbmw-motorrad-abudhabi.com bmw-motorrad-authorities.combmw-motorrad-dubai.combmw-motorrad-motorsport.com!bmw-motorrad-now-or-never.com&"bmw-motorrad-service-inclusive.combmw-motorrad-test-ride.combmw-motorrad.atbmw-motorrad.bebmw-motorrad.bgbmw-motorrad.bhbmw-motorrad.bobmw-motorrad.cabmw-motorrad.chbmw-motorrad.clbmw-motorrad.cobmw-motorrad.co.idbmw-motorrad.co.krbmw-motorrad.co.nzbmw-motorrad.co.thbmw-motorrad.co.ukbmw-motorrad.co.zabmw-motorrad.combmw-motorrad.com.arbmw-motorrad.com.aubmw-motorrad.com.brbmw-motorrad.com.dobmw-motorrad.com.hrbmw-motorrad.com.mxbmw-motorrad.com.mybmw-motorrad.com.pebmw-motorrad.com.pybmw-motorrad.com.trbmw-motorrad.crbmw-motorrad.czbmw-motorrad.debmw-motorrad.dkbmw-motorrad.dzbmw-motorrad.ecbmw-motorrad.eebmw-motorrad.esbmw-motorrad.fibmw-motorrad.frbmw-motorrad.grbmw-motorrad.gtbmw-motorrad.hubmw-motorrad.inbmw-motorrad.itbmw-motorrad.jpbmw-motorrad.lubmw-motorrad.mabmw-motorrad.nlbmw-motorrad.nobmw-motorrad.pabmw-motorrad.plbmw-motorrad.ptbmw-motorrad.robmw-motorrad.rsbmw-motorrad.rubmw-motorrad.sabmw-motorrad.sebmw-motorrad.sibmw-motorrad.skbmw-motorrad.svbmw-motorrad.twbmw-motorrad.uabmw-motorrad.uybmw-motorsport.combmw-mountains.combmw-museum.combmw-museum.netbmw-nigeria.com bmw-oman.com bmw-ottawa.cabmw-pakistan.combmw-plant-munich.combmw-pma.com.sgbmw-powertrain.combmw-product-highlights.com bmw-qatar.combmw-routes.com -bmw-rp.combmw-rrdays.combmw-saudiarabia.combmw-security-vehicles.combmw-special-sales.combmw-sports.com bmw-sudan.combmw-tahiti.combmw-tunisia.combmw-vancouver.ca bmw-voli.me bmw-welt.com bmw-welt.net bmw-welt.tvbmw-werk-berlin.de bmw-world.com bmw-world.net bmw-world.tvbmw-worldfinal.combmw-yachtsport.com -bmw-yeg.ca bmw-yemen.com -bmw.am -bmw.at -bmw.az -bmw.ba -bmw.bb -bmw.be -bmw.bg -bmw.bm -bmw.bs -bmw.by -bmw.ca -bmw.cc -bmw.ch -bmw.cl  bmw.co.ao  bmw.co.cr  bmw.co.id  bmw.co.il  bmw.co.jp  bmw.co.ke  bmw.co.kr  bmw.co.nz  bmw.co.th  bmw.co.uk  bmw.co.za bmw.com -bmw.com.ar -bmw.com.au -bmw.com.bd -bmw.com.bn -bmw.com.bo -bmw.com.br -bmw.com.cn -cn -bmw.com.co -bmw.com.cy -bmw.com.do -bmw.com.ec -bmw.com.ge -bmw.com.gt -bmw.com.kh -bmw.com.ky -bmw.com.mk -bmw.com.mo -bmw.com.mt -bmw.com.mx -bmw.com.my -bmw.com.ni -bmw.com.pa -bmw.com.pe -bmw.com.ph -bmw.com.py -bmw.com.sg -bmw.com.sv -bmw.com.tr -bmw.com.tw -bmw.com.uy -bmw.com.ve -bmw.cw -bmw.cz -bmw.de -bmw.dk -bmw.dz -bmw.ee -bmw.es -bmw.fi -bmw.fr -bmw.gp -bmw.gr -bmw.hn -bmw.hr -bmw.ht -bmw.hu -bmw.ie -bmw.in -bmw.is -bmw.it -bmw.kg -bmw.kz -bmw.lc -bmw.lk -bmw.lt -bmw.lu -bmw.lv -bmw.ly -bmw.ma -bmw.md -bmw.mn -bmw.mq -bmw.mu -bmw.nc -bmw.nl -bmw.no -bmw.pl -bmw.ps -bmw.pt -bmw.re -bmw.ro -bmw.rs -bmw.se -bmw.si -bmw.sk -bmw.sn -bmw.tm -bmw.tt -bmw.ua -bmw.uz -bmw.vn bmwarchiv.at bmwarchiv.debmwartjourney.com bmwasia.combmwauslieferungszentrum.com bmwbikes.com  bmwbkk.de -bmwccrc.cabmwcenternet.combmwchampionship.combmwchampionshipusa.combmwcharitygolf.combmwcitychallenge.combmwclassic.combmwcolorado.combmwconnecteddrive.combmwcustomapparel.com bmwdcsnet.netbmwdealercareers.combmwdealerdirect.combmwdelawarevalley.com bmwfilms.combmwgroup-classic-heart.combmwgroup-classic.combmwgroup-gaad.combmwgroup-plants.combmwgroup-posdigital.combmwgroup-werke.com bmwgroup.at bmwgroup.com bmwgroup.net bmwgroup.jobsbmwgroupclassic.combmwgroupdesignworks.combmwgroupdirect.combmwgroupfs.combmwgroupinfobahn.combmwgroupna.com  bmwhk.com bmwi.ca bmwi.jpbmwiventures.combmwjamaica.com -bmwlat.combmwlifestyle.ca bmwm.combmwmagazine.combmwmagazine.de bmwmass.com bmwmass.net  bmwmc.netbmwmontreal.cabmwmotorcycles.combmwmotorcycleusa.combmwmotorrad.co.krbmwmotorrad.com.aubmwmotorrad.com.phbmwmotorrad.com.sgbmwmotorradhk.combmwmotorshowblog.combmwmotorsport.combmwmperformance.com bmwmuseum.netbmwmyanmar.combmwofannapolis.combmwofcentralpa.combmwoftulsa.combmwondemandusa.com bmworegon.combmwperformancecenter.combmwpittsburgh.combmwproductnews.com bmwsafari.com -bmwsfl.com -bmwsfl.net -bmwshop.cabmwspecialoffers.ca bmwsports.combmwstartupgarage.com bmwstep.combmwstepconnections.combmwsummerschool.combmwtampabay.com -bmwusa.combmwusatires.combmwusaservice.combmwusfactory.combmwusrideracademy.combmwvalueservice.combmwwholesaleconnect.com bmwworld.com bmwworld.net bmwworld.tvbmwzentrum.combrand-protection-team.comcencoastbmw.com chargenow.comchargenowusa.comchicagolandbmw.comconnected-drive.com daytonbmw.comdealerspeed.net desertbmw.com dreambmw.ca grupobmw.com guambmw.comhackthedrive.com hawaiibmw.com iaablog.comlasvegasbmw.com m-power.comminneapolisbmw.comminneapolisbmw.netmotorshowblog.com mybmw.ca  mybmw.comnewhampshirebmw.com  nmbmw.comocbmwdealers.comoffresspecialesbmw.ca park-now.com parkinfo.com -pdxbmw.com -phxbmw.compre-bmwgroup.jobspugetsoundbmw.com rentaride.com rentaride.derhodeislandbmw.comsdcountybmw.comshopbmwmotorcycles.comshopbmwusa.com socalbmw.comstlouisbmw.netthe-m-festival.comtristatebmw.comvancouverbmwretailers.cawashingtondcbmw.com alfera.com.hk alfera.com.my  alfera.in  alfera.myalphera-finance.co.inalphera-finance.com.hkalphera-finance.in -alphera.ca alphera.co.in alphera.co.nz alphera.comalphera.com.esalphera.com.hkalphera.com.my -alphera.de -alphera.es -alphera.ie -alphera.in -alphera.my alphera.netalpherafinance.com.hkalpherafinancialservices.esalpherafinancialservices.in alpherafs.caalpherafs.co.nzalpherafs.com.hkalpherafs.com.my alpherafs.ie alpherafs.in alpherafs.my premiumfs.de alphabet.asia alphabet.at alphabet.be alphabet.biz alphabet.chalphabet.co.hualphabet.co.ukalphabet.co.za alphabet.comalphabet.com.cn -cnalphabet.com.esalphabet.com.lvalphabet.com.mxalphabet.com.plalphabet.com.pt alphabet.cz alphabet.de alphabet.es alphabet.eu alphabet.fr alphabet.info alphabet.jp alphabet.lt alphabet.lu alphabet.lv alphabet.mxalphabet.net.cn -cn alphabet.noalphabet.org.uk alphabet.pl alphabet.pt alphabet.uk alphabet.usalphabetfinance.netelectricluxury.comrolls-roycecullinan.comrolls-roycemotorcars.comrolls-roycemotorcarsna.com  rrtis.comspiritofecstasy.com - - INTEL-DEV -01.org hyperscan.iointellinuxgraphics.comintellinuxgraphics.net -oneapi.comopenvinotoolkit.orgsnap-telemetry.io -acpica.com clearlinux.cn -cnclearlinux.org -ospray.net -ospray.org - -NAVER -lin.ee line.me line.naver.jp line-apps.com line-cdn.net line-scdn.net -nhncorp.jp grafolio.com  naver.com naver.jp  naver.net navercorp.com  plug.game pstatic.net -/ - SNAPCRAFT snapcraft.io snapstore.io -" - ADBLOCKPLUSadblockplus.org -b - -EMOJIPEDIAemojipedia.orgworldemojiawards.comworldemojiday.com xn--yt8h.la - -ORACLE -oracle -oracle.comoraclecloud.comoraclefoundation.org oracleimg.comateam-oracle.com -bronto.comcovid19-rx.org covid19rx.org sun.com addthis.comaddthiscdn.comaddthisedge.comjava java.com java.net -( -CAIJINGLENGYANcaijinglengyan.com -k -SLING dishworld.comslinginternational.com  sling.commovenetworks.com -movetv.com -E -ETHEREUM -devcon.orgethereum.foundation ethereum.org -D -ICBCicbc icbc-ltd.com icbc-us.com icbc.com.cn - -JIYUKOBOjiyu-kobo.co.jp -8 - KINGKONGLIVEkingkong.com.twkingkongapp.com - -SONY playstationplaystation.complaystation.net sonyentertainmentnetwork.comimageworks.commotionpictureser.com playshow.ioplayshowtv.comsonyclassics.comsonypictures.comsonypicturesanimation.comsonypicturesmuseum.comsonypicturesstudios.comsonypicturestelevision.com#sonypicturestelevisiongames.com wheelpop.com -wwtbam.comyourfantasybeginsnow.comsonyginzasonypark.jpsony-africa.com sony-asia.comsony-europe.comsony-hes.co.jpsony-latin.com sony-mea.comsony-olympus-medical.comsony-promotion.eusony-semicon.co.jp sony.at sony.ba sony.be sony.bg sony.ca sony.ch sony.cl -sony.co.cr -sony.co.id -sony.co.in -sony.co.jp -sony.co.kr -sony.co.nz -sony.co.th -sony.co.uk sony.com sony.com.ar sony.com.au sony.com.bo sony.com.br sony.com.co sony.com.do sony.com.ec sony.com.gt sony.com.hk sony.com.hn sony.com.mk sony.com.mx sony.com.my sony.com.ni sony.com.pa sony.com.pe sony.com.ph sony.com.sg sony.com.sv sony.com.tr sony.com.tw sony.com.vn sony.cz sony.de sony.dk sony.ee sony.es sony.eu sony.fi sony.fr sony.gr sony.hr sony.hu sony.ie sony.it sony.kz sony.lt sony.lu sony.lv sony.net sony.nl sony.no sony.pl sony.pt sony.ro sony.rs sony.ru sony.se sony.si sony.sk sony.ua sonybo.co.jp sonybsc.comsonybuilding.jpsonydesign.com sonydna.comsonyglobalsolutions.jp sonykigyo.jpsonylatvija.com -sonypcl.jpsonyprotechnosupport.co.jpsonystoragemedia.co.jp sonytc.co.jp  sovec.net -* -BETTEREXPLAINEDbetterexplained.com - - -CODECADEMYcodecademy.com -3 -ELEME -ele.me elemecdn.com eleme.cn - -VEET veet.at veet.be veet.ca veet.ch veet.clveet.cn -cn -veet.co.id -veet.co.in -veet.co.kr -veet.co.nz -veet.co.uk -veet.co.za veet.com veet.com.ar veet.com.au veet.com.bd veet.com.br veet.com.cn -cn veet.com.co veet.com.hk veet.com.mx veet.com.my veet.com.ph veet.com.pk veet.com.sg veet.com.tr veet.de veet.dk veet.es veet.fi veet.fr veet.hu veet.jp veet.nl veet.no veet.pt veet.ro veet.ru veet.se veet.tv veet.usveetarabia.comveetbangladesh.comveetcentroamerica.com veetclub.it -' -WATCHOUT watchout.tw -waa.tw - -SOFTBANK softbank sb-mobile.jpsb-telecom.netsoftbank-ipo.comsoftbank-jp.comsoftbank-robotics.comsoftbank-telecom.comsoftbank-telecom.net softbank.jp softbank.tvsoftbankbb.comsoftbankbb.netsoftbankci.comsoftbankhawksstore.jpsoftbankmobile.netsoftbankrobotics.comsoftbanktelecom.comsoftbankusa.netsoftbankventuresasia.comsoftbankworld.com -% -ZHIHU  zhihu.com  zhimg.com -+ -AFDIAN -afdian.net afdiancdn.com - -KUAISHOU gifshow.com ksapisrv.com kuaishou.com -yximgs.com log-sdk.gifshow.com -adswlog.kuaishou.com -ads -~ -NEWSCORP newscorp.com dowjones.com dowjones.iodowjoneson.com wsj.com wsj.net barrons.com - -GFYCAT -gfycat.com -. -ASPROEX asproex.comasproexapi.com - -CTYUN ctyun.online -n -ESPN espn.com espncdn.com watchespn.comespn.hb.omtrdc.netespndotcom.tt.omtrdc.net - -PINCONG pincong.rocks -+ -TYPEKIT typekit.com typekit.net -7 -VANCL  vancl.com vanclimg.com  vcimg.com - -ALIYUNalibabadns.com -alicdn.com alidayu.com -alidns.comaliyun-inc.comaliyun-iot-share.com -aliyun.com aliyuncdn.com aliyuncs.com hichina.comica-alliance.org mxhichina.com - -CODE code.org -+ - ONESIGNAL-ADS onesignal.com -ads -/ -CLEARBITJS-ADSx.clearbitjs.com -ads -, -DUYAOSS duyaoss.com duyaossr.com - - -FREEBEACONfreebeacon.com - -REMIREPO remirepo.net -" - THEPORNDUDEtheporndude.com - -BLIZZARD blizzard.cn -cn blizzard.comblizzardgearstore.comblz-contentstack.com blzstatic.cn -cn -battle.netbattlenet.com.cn -cn bnet.163.com -cnblizzard.nefficient.co.krblzddist1-a.akamaihd.netblzddistkr1-a.akamaihd.netbnetcmsus-a.akamaihd.netbnetproduct-a.akamaihd.netbnetshopus.akamaized.net diablo3.comdiabloimmortal.comdi.res.netease.com -cn!diablo3.nosdn.127.net -cnfiresidegatherings.complayhearthstone.com%hearthstone.nosdn.127.net -cnheroesofthestorm.com%hearthstone.nosdn.127.net -cn"heroes.nos.netease.com -cnplayoverwatch.com#overwatch.nosdn.127.net -cn starcraft.comstarcraft2.comsc2.nosdn.127.net -cnplaywarcraft3.comworldofwarcraft.com wowchina.com -cnwow.nosdn.127.net -cnblizzcon-a.akamaihd.netblz.nosdn.127.net -cnblzmedia-a.akamaihd.netblznav.akamaized.net - - BLOOMBERG  bloomberg bbgevent.com bbhub.io -bbthat.com bgov.com bloomberg.cnbloomberg.co.jpbloomberg.co.kr bloomberg.combloomberg.com.brbloombergapps.combloombergarcade.combloombergbeta.combloombergbna.combloombergbreakaway.combloombergbriefs.combloombergchina.combloombergenvironment.combloombergforeducation.combloomberggovernment.combloombergindices.combloombergindustry.combloomberglabs.combloomberglaw.combloomberglive.combloomberglp.combloombergmedia.combloombergnext.combloombergpolarlake.combloombergprep.combloombergquint.combloombergradio.combloombergsef.combloombergspace.combloombergstatus.combloombergsurvey.combloombergtax.combloombergtax1.combloombergtaxtech.com!bloombergtradingchallenge.combloombergtv.mnbloombergvault.combloombergview.comblpcareers.com blpevents.comblpprofessional.com bmia.org bnef.combusinessweekmag.comlondonmithraeum.comneweconomyforum.comnewenergyfinance.comtechatbloomberg.com - -CAOLIU t66y.com - -XIAOMI -mi.com -mi-img.com  mifile.cn miui.com -miwifi.com  xiaomi.cn -xiaomi.com -xiaomi.netxiaomiyoupin.com -f -PHOENIX  ifeng.com ifengimg.comphoenixcenter.cn fengedu.comifengweekly.com -L -TLD-!CNacadaeafagalamanaoaqaratauawaxazbabgbhbibjbmbnbobrbsbvbwbybzcacdcgclcmcrcucwcxcyczdedkdzecegehereseufifjfkfofrgagbgdgeghgiglgmgngrgsgtgugwgyhkhnhthuieiliniqirjejmjojpkekhkiknkpkrkwkykzlblclilklrlsltlulymamcmdmfmgmhmkmnmompmqmrmtmumwmxmymznancnenfngninlnonpnrnunzompapepfpgphplpnrerorurwsascsdsesgsisjskslsnsrssstsusvsxsysztctdtfthtjtktltmtntotptrtttwtzuaugukumusuyuzvavcvevgvivnvuwfytzazmzwaflmlbnbanflnra averybbbbcgcebcrsdnbdnpdunsdvagerni globalxice locus mckinsey olayangroup -olayan praxisapseek -vistaprint vistawtc xerox athletabananarepublic  bloomberg boots calvinklein cartier chloegap gucci -hermesiwcjlc  lancaster lipsyloft mango  montblanc mrporter -netaporter -nextdirectnextnike oldnavy omega panerai  patagonia -piaget  piperlimepolo redstone -swatchaarp -abbott -abbvie aetna alcon -anthembms -boehringer caremore -cialis dabur  freseniusjnj merck mutuelle -sanofiaeg -airbus -alstom -aramco -blanco bosch -bostik -chesapeakechk crown -doosan -dupontflirfls flsmidth  gecompany grainger hitachi  honeywellifm ipirangajio komatsukoneksb  kuokgroup leclerclidl linde lottemanmtr reliance rexrothril rockwoolrwesandvikcoromant sandvik sapphiresca -schaeffler schmidt schwarzgroup schwarz senersewsncf statoil total -vanish -walter -warman weberweir woodsidezara zippo agakhanakdn -ansons -bharti -extraspace genting glean ikano -imamat  ladbrokes -livestrongmaif -mattel shell transformersvanawme astriumcern -emerck  hisamitsu lilly lundbeck lupin merckmsdmsdmtpc -otsuka -pfizer stadateva toray weather auspost deutschepostdhl epost fedex -pitneyaaa -abarthadac  alfaromeoaudi bentleybmw bridgestone bugatti buick cadillaccase -caseih  chevrolet chevy chrysler -datsun dodge -dunlop ferrarifiat  firestonefordgmc goodyear honda hyundai infiniti iveco -jaguarjeepkia lamborghini -lancia  landrover lexus lincoln maseratimini -mitsubishi mopar -newholland -nissanramseatsrt -suzuki -tatamotors -toyotatvs uconnect -volkswagen volvoaigo android apple bestbuy brother canondell digikey fujitsu  fujixerox  goldpointibm nikon nokia olympus  panasonic philips pioneer samsung sharpsony toshiba  yodobashi  allfinanzallfinanzberaterallfinanzberatungallyamericanexpressamexampanz banamex barclaycard barclaysbbtbbvabnl -bnpparibasbofa bradesco -capitalone cashbackbonuscbacfa chasecimb citadelciti commbank deloitte discover everbank fidelity hdfcbankhdfchsbcitaujcbjll jpmorganchase jpmorgankfhkpmg lacaixa lasalle lixil lplfinanciallplnab nadex netbanknorthwesternmutualopen -pictetpncpohl  pramerica -prudentialprupwcsaxosbiscbscor shriram softbank  statebank temasek -transunion ubankubs vanguardvisa wolterskluwerafamilycompany  aquarellediyduck -finish glade  homedepot  homegoods mrmuscleoffraid rightathome  scjohnsonaig allstateamericanfamily amfam amicaaxa chartis esurance farmers  goodhands guardian guardianlife iselect metlifemma -mutual -nationwide -nissaynorthlandinsurance -onyourside progressive redumbrella  statefarm -swiftcover thehartfordtiaatravelersinsurance  travelers travelguardtrvvig beats blockbusterbway netflix playstation showtimetab williamhillxboxasda bloomingdales -george  homesense justforulego macys  marshallsobiqvc safeway samsclub shopyourway staples -targetthd tiffany -tjmaxxtjx -tkmaxxvons walmartabb  accentureaceraco -active -airtel -amazonandaolawsaxis azure chatr -chrome cisco comcast -connectorscscdclkdish -docomodtvdwg emerson epson ericsson etisalat  fairwindsfido -flickr frogans frontierftrgbizgeaggeegle gmailgmogmx godaddygoog -googlegoogreeguge hangouthkt hotmailhtc -hughesieee iinet infosysinfy intel -intuitjavajcpjmpjprs juniperkddi -kindle -konamikpn lanxess liaison -lockermeo  microsoftmihmiimint mitekmit -mobily -mozaicmtn multichoice naspersnec neustar nexusnico -nortonntt -officeollo -oracle -orangeovh patchpayupccwqtel quest  richardli ricoh -rogers -sakurasaposassessfrshaw skolkovo skydrive skype sling smart stcgroupstc symantectatatcitdk telecity -telefonica terra translations  ultrabook unicornuol verisignvivavivo windows xfinity -xperia yahoo yamaxun -yandex yellowpages youtube -zappos -chanel clinique garnier  kerastase -kiehls lamer lancome -loreal -matrix -maybelline -natura origins -redkensvr barefoot beknown cityeatscookingchannel -cuisinella delmonte edekafage ferrero foodnetworkfood gallo heinz -kindermcd  mcdonalds pamperedchef -rocher -wilmarxn--3oq18vl8pn36axn--55qx5d8y0buji4b870uxn--5su34j936bgsgxn--b4w605ferd xn--cckwcxetd -xn--cg4bkixn--dkwm73cwpn xn--flw351exn--fzys8d69uvgmxn--hxt035cmppuelxn--hxt035czzpfflxn--j6w470d71isscxn--jlq480n2rgxn--jlq61u9w7bxn--kcrx77d1x4axn--kcrx7bb75ajk3bxn--mgba3a3ejtxn--mgba7c0bbn0axn--mgbaakc7dvfxn--mgbb9fbpob xn--ngbe9e0a xn--pgb3ceoj xn--qcka1pmcxn--w4r85el8fhu5dnra xn--w4rs40l avianca caravan changiairport cipriani clubmed delta firmdale -hilton hyatt kerryhotelskerrylogisiticskerryproperties marriott orientexpress  shangrila tradershotelstui -viking -virgin -webjetablecbre chintai  frontdoor -lefrakabc africamagicartebbc  canalpluscbncbsdstv -eurovisionfox -gallup globogotv guardianmediahbohgtvimdbitv -kyknetmnet movistar mzansimagicnhk nowtvsbs sevensky spiegel starhub -supersport theguardian travelchannelweatherchannel bauhaus latrobe -monashrmit - - WIKILEAKS wikileaks.org - -BITQUICK bitquick.co -> - CATEGORY-DEValpinelinux.org anaconda.com anaconda.org conda.io android.comandroidify.com -apache.orgappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cncalendarserver.orgapple-cloudkit.com cups.orgdeveloper.apple.comdevimages-cdn.apple.comdevstreaming-cdn.apple.comfoundationdb.orgmacosforge.org macruby.com macruby.net macruby.orgapple-mapkit.complaygrounds-cdn.apple.com carekit.orgresearchandcare.orgresearchkit.cn -cnresearchkit.com.cn -cnresearchkit.hkresearchkit.netresearchkit.orgresearchkit.tvservicetalk.io -webkit.org webkitgtk.org wpewebkit.orgwebobjects.com-)appleworldwidedeveloper.hb-api.omtrdc.net)%appleworldwidedeveloper.sc.omtrdc.net#p-events-delivery.akamaized.net archlinux.orgarchlinuxarm.org atom.io -badgen.netbootstrapcdn.comgetbootstrap.combraventures.com braveux.com -compass.isfightforux.comforthebadge.comstockagainstphotography.com launchpad.netlaunchpadlibrarian.comlaunchpadlibrarian.netlaunchpadlibrarian.org snapcraft.io snapstore.io -ubuntu.com -ubuntu.netubuntuforums.org canonical.com  cdnjs.com -centos.orgchocolatey.org clojure.org -cnpmjs.org -cygwin.com -cython.org dart.dev dartlang.org dartpad.dev -debian.org -dotdeb.org  deno.landcompose-spec.io -docker.com  docker.ioelixir-lang.org -elrepo.org embed.ly embedly.com playerjs.io -erlang.orgaccountkit.comatscaleconference.com botorch.org -buck.build buckbuild.comcomponentkit.org draftjs.org -f8.comfacebookappcenter.infofacebookappcenter.netfacebookappcenter.orgfacebookconnect.comfacebookdevelopergarage.comfaciometrics.com fasttext.cc fbf8.com fbinfer.com fblitho.com fbredex.com -fbrell.com flow.dev flow.org flowtype.org frescolib.org hacklang.org hhvm.commakeitopen.com mcrouter.net mcrouter.orgmessengerdevelopers.com -ogp.meopengraphprotocol.comopengraphprotocol.org  parse.com pyrobot.org  react.com reactjs.com reactjs.org recoiljs.org rocksdb.com rocksdb.net rocksdb.orgyogalayout.comdevelopers.facebook.com fastlane.cifastlane.tools getfedora.orgfedoraforum.orgfedoraproject.orgfedoramagazine.org -ffmpeg.org flathub.org flatpak.org flutter.devflutterapp.comfontawesome.comfortawesome.com freebsd.org  fury.blog fury.co fury.dev  fury.help fury.io gemfury.com -gentoo.orggetcomposer.org git-scm.com gitbook.com -gitbook.iogitbook.zendesk.com  npmjs.com npm.communitydependabot.com -git.io github.blog -github.comgithub.community  github.io githubapp.comgithubassets.comgithubhackathon.comgithubstatus.comgithubusercontent.com/+github-avatars.oss-cn-hongkong.aliyuncs.comFBgithub-production-release-asset-[0-9a-za-z]{6}\.s3\.amazonaws\.com -gitlab.com  gitlab.io -gitlab.netgitlab-static.net.*gitlab-assets.oss-cn-hongkong.aliyuncs.com gnu.org go-lang.com go-lang.net go-lang.org -go.dev  godoc.org -golang.com -golang.net -golang.org  consul.io hashicorp.comnomadproject.io  packer.io terraform.io vagrantup.comvaultproject.io haskell.orghaskellstack.org stackage.org brew.shhomebrew.bintray.com -01.org hyperscan.iointellinuxgraphics.comintellinuxgraphics.net -oneapi.comopenvinotoolkit.orgsnap-telemetry.io -acpica.com clearlinux.cn -cnclearlinux.org -ospray.net -ospray.org -ius.iojava java.com java.net -jenkins.io jetbrains.com jetbrains.net datalore.iokotlinlang.org  jfrog.com  jfrog.org bintray.com!d29vzk4ow07wi7.cloudfront.net -js.org jsdelivr.net jsdelivr.comcdn.jsdelivr.net -cn jupyter.org kali.org kubernetes.io -k8s.iolinuxfromscratch.org linuxmint.com lua.org macports.org mariadb.orgadaptivecards.ioapi-extractor.com -apisof.net appcenter.ms -blazor.netbotframework.comcodethemicrobit.comdevopsassessment.net dot.netgamesstack.comgraphengine.ioinsiderdevtour.com -jwt.ms microbit.orgmicrosoftadc.commicrosoftgamestack.commicrosoftiotinsiderlabs.commicrosoftreactor.cn -cn#microsoftreactor.com.cn -cnmicrosoftreactor.infomicrosoftreactor.netmicrosoftreactor.orgmicrosoftsilverlight.commicrosoftsilverlight.orgmicrosoftsilverlight.netmicrosoftsqlserver.com  mmdnn.commono-project.com msdn.commsinnovationchallenge.commspairlift.commsropendata.com  nuget.org nugettest.orgopentranslatorstothings.orgposhtestgallery.compwabuilder.comreactorms.com.cn -cn -sankie.netsqlserveronlinux.comtimelinestoryteller.comuwpcommunitytoolkit.comvisualstudio-staging.comvisualstudio.comvisualstudio.covisualstudio.co.ukvisualstudio.euvisualstudio.net vfsforgit.com vfsforgit.org vsassets.io xamarin.comdeveloper.microsoft.comdtlgalleryint.cloudapp.net poshtestgallery.cloudapp.net"psg-int-centralus.cloudapp.netpsg-int-eastus.cloudapp.net  mingw.org mongodb.com mongodb.org  mysql.com  nginx.com  nginx.org  nixos.orgnodesource.comnotepad-plus-plus.org -openai.com openresty.org openssl.org opensuse.org openwrt.orglede-project.org packagist.org pcre.org perl.org cpan.org metacpan.org phantomjs.org php.netpolymer-project.orgpolymerproject.orgpostgresql.org -python.orgpythonhosted.org pypa.io pypi.org qt.ioqt-project.org r-project.orgraspberrypi.org raspbian.orgreadthedocs-hosted.comreadthedocs.comreadthedocs.ioreadthedocs.org redis.io redislabs.com remirepo.net rpmfusion.org ruby-lang.org rubygems.orgscala-lang.org scala-sbt.orgscientificlinux.org scoop.sh -shields.io -sqlite.org askubuntu.comblogoverflow.commathoverflow.netserverfault.com sstatic.net stackapps.com stackauth.comstackexchange.com stackmod.blogstackoverflow.blogstackoverflow.comstackoverflow.emailstackoverflowbusiness.comstacksnippets.net superuser.com tex-talk.netthesffblog.commystrikingly.comstrikingly.comstrikinglycdn.comsublimetext.com -kernel.org  linux.org amp.devampproject.comampproject.netampproject.orgcdn.ampproject.org -cnamp.akamaized.netelectronjs.org -jquery.comjquerymobile.com jqueryui.com mochajs.org -nodejs.org openjsf.orglfai.foundationlinuxfoundation.org  unity.com unity3d.com!unityads.unity3d.com -ads#unityads.unitychina.cn -ads  unpkg.com -v8.dev v8project.org videojs.com videolan.org vim.org wireshark.org x.orgxda-developers.com xda-cdn.com xposed.info yarnpkg.com zsh.org -# - DATFOUNDATIONdat.foundation -, -HITUN hitun.ioxn--90wwvt03e.com -) -NPMJS  npmjs.com npm.community - - SWISSSIGN digital-id.ch digitalid.chmanaged-pki.chmanaged-pki.de pki-post.ch pki-posta.ch pki-poste.chroot-signing.chrootsigning.comssl-certificate.ch -swissid.ch swissign.com swissign.liswisssign-group.chswisssign-group.comswisssign-group.li swisssign.ch swisssign.com swisssign.li swisssign.net swisssign.orgswisssigner.chswisssigner.comswisssigngroup.chswisssigngroup.comswissstick.com swisstsa.ch swisstsa.li trustdoc.ch trustsign.ch zert.ch -H -ACFUN acfun.cn aixifan.com apilog-web.acfun.cn -ads -. -CABLETVcabletv.com.hk i-cable.com - -FANDOM cursecdn.com -fandom.com  wikia.com  wikia.org dndbeyond.com futhead.com gamepedia.com muthead.comwikia.nocookie.netfandom.zendesk.com - -CHIPHELL chiphell.com - -DMM-ADS bluekai.com -adsfout.jp -ads gmossp-sp.jp -adsi-mobile.co.jp -ads im-apps.net -ads impact-ad.jp -ads ladsp.com -ads microad.co.jp -ads -microad.jp -ads socdm.com -ads - - -JINRISHICIjinrishici.com -q -CATEGORY-MEDIA 9cdn.net9nation.com.au 9news.com.au 9now.com.aufreeviewplus.net.au -mi9.com.au -mi9cdn.com nine.com.aunineentertainment.com.aunineentertainmentco.com.auninemediaroom.com.auninemsn.com.austatic9.net.au yourtv.com.au9to5google.com 9to5mac.com9to5terminal.com 9to5toys.com dronedj.com electrek.cospaceexplored.com afp.com afpforum.com -ajplus.net aljazeera.com aljazeera.netpalestineremix.comsarajevopodopsadom.comsrebrenica360.com syhacked.comamericasvoice.news -ap.org -apnews.comappledaily.comappledaily.com.hkappledaily.com.tw appledaily.hkapplefruity.comapplehealth.com.hk -atnext.combestmallawards.com deluxe.com.hk eracom.com.tw next.hknextdigital.com.hknextdigital.com.twnextfilm.com.hknextmag.com.tw nextmedia.comnextmedia.com.tw nextmgz.comnextplus.com.hk nexttv.com.twnextwork.com.hknextwork.com.tw nextwork.hk nextwork.tw nxtdig.com.hk nxtdig.com.tw omoplanet.com privilege.hk privilege.tw sharpdaily.tw tomonews.nettwnextdigital.comappleinsider.com  asahi.com asahicom.jpasahishimbun.sc.omtrdc.netbbc  bbc.co.uk bbc.com -bbci.co.uk bbgwatch.com  bloomberg bbgevent.com bbhub.io -bbthat.com bgov.com bloomberg.cnbloomberg.co.jpbloomberg.co.kr bloomberg.combloomberg.com.brbloombergapps.combloombergarcade.combloombergbeta.combloombergbna.combloombergbreakaway.combloombergbriefs.combloombergchina.combloombergenvironment.combloombergforeducation.combloomberggovernment.combloombergindices.combloombergindustry.combloomberglabs.combloomberglaw.combloomberglive.combloomberglp.combloombergmedia.combloombergnext.combloombergpolarlake.combloombergprep.combloombergquint.combloombergradio.combloombergsef.combloombergspace.combloombergstatus.combloombergsurvey.combloombergtax.combloombergtax1.combloombergtaxtech.com!bloombergtradingchallenge.combloombergtv.mnbloombergvault.combloombergview.comblpcareers.com blpevents.comblpprofessional.com bmia.org bnef.combusinessweekmag.comlondonmithraeum.comneweconomyforum.comnewenergyfinance.comtechatbloomberg.com  boxun.combowenpress.com breitbart.comcabletv.com.hk i-cable.comcaijinglengyan.comcbs showtime cbs.comcbscorporation.com cbsi.com -cbsi.video -cbsiam.com  cbsig.net -cbsimg.netcbsinteractive.comcbsistatic.com cbsivideo.com cbsnews.comcbspressexpress.com cbssports.com cbsstatic.com cbssvideo.com -viacbs.com -viacom.com viacomcbs.comviacomcbspressexpress.com paramount.comchinadecoding.comchinadigitaltimes.net cnbc.com -cnbcfm.com -nbcuni.comnbcudigitaladops.com cnn.com -cnn.io cnnarabic.com cnnlabs.com cnnmoney.ch cnnmoney.comcnnmoneystream.comcnnpolitics.com -cup.com.hk d100.net  anm.co.uk  dailym.aidailymail.co.uk dailymail.comdailymail.com.au dailymail.dkdailymailonline.comdmgmediaprivacy.co.ukmailhealth.commailonline.co.ukmailonline.commailonsunday.co.ukmailonsunday.ie metro.co.uk -mol.imthisismoney.co.uk -thismon.ee -dw.com dajiyuan.com dajiyuan.euepochmediagroup.comepochstories.com epochtime.comepochtimes-romania.comepochtimes.co.ilepochtimes.co.krepochtimes.co.ukepochtimes.comepochtimes.com.auepochtimes.com.brepochtimes.com.sgepochtimes.com.twepochtimes.com.ua epochtimes.cz epochtimes.de epochtimes.eu epochtimes.fr epochtimes.it epochtimes.jp epochtimes.nl epochtimes.pl epochtimes.ru epochtimes.seepochtimestr.com epochweek.comepochweekly.com erabaru.netlagranepoca.com ntd.com ntdtv.ca ntdtv.co.kr  ntdtv.com ntdtv.com.tw ntdtv.jp ntdtv.kr  ntdtv.org ntdtv.ru ntdtvla.com  ntdvn.comtheepochtimes.comtheepochtimessubscribe.comvelkaepocha.cz -forbes.com forbesimg.comfoxfox-corporation.com fox-news.com fox.com -fox.tv fox10.tv fox10news.comfox10phoenix.com  fox11.comfox13memphis.com fox13news.com  fox23.comfox23maine.com -fox247.com  fox247.tv  fox26.comfox26houston.comfox28media.com  fox29.comfox2detroit.com fox2news.com  fox32.comfox32chicago.comfox35orlando.comfox38corpuschristi.com fox42kptm.com  fox46.comfox46charlotte.com  fox47.com fox49.tv fox4news.com fox51tns.netfox5atlanta.com -fox5dc.com -fox5ny.com fox5storm.com fox6now.com fox7.comfox7austin.com fox9.comfoxacrossamerica.comfoxaffiliateportal.comfoxandfriends.com -foxbet.comfoxbusiness.comfoxbusiness.tvfoxbusinessgo.comfoxcanvasroom.comfoxcareers.comfoxcharlotte.com foxcincy.com foxcincy.jobs foxcincy.netfoxcollegesports.comfoxcorporation.comfoxcreativeuniversity.com foxcredit.com foxcredit.org foxd.tv -foxdcg.comfoxdeportes.comfoxdeportes.netfoxdeportes.tvfoxdigitalmovies.com foxdoua.comfoxentertainment.com -foxest.com -foxfaq.com -foxfdm.com foxfiles.com -foxinc.com foxkansas.com  foxla.com foxla.tvfoxlexington.comfoxmediacloud.com foxnation.comfoxnebraska.com -foxneo.comfoxneodigital.comfoxnetworks.infofoxnetworksinfo.com -foxnews.cc foxnews.com foxnews.net foxnews.org -foxnews.tvfoxnewsaffiliates.comfoxnewsaroundtheworld.comfoxnewsb2b.comfoxnewschannel.com foxnewsgo.net foxnewsgo.org foxnewsgo.tvfoxnewshealth.comfoxnewslatino.comfoxnewsmagazine.comfoxnewsnetwork.comfoxnewsopinion.comfoxnewspodcasts.comfoxnewspolitics.comfoxnewsradio.comfoxnewsrundown.comfoxnewssunday.com  foxon.comfoxphiladelphia.com foxpoker.com  foxrad.io foxredeem.comfoxrelease.comfoxrichmond.com foxrobots.comfoxsmallbusinesscenter.comfoxsmallbusinesscenter.netfoxsmallbusinesscenter.org foxsoccer.net foxsoccer.tvfoxsoccermatchpass.comfoxsoccerplus.comfoxsoccerplus.netfoxsoccerplus.tvfoxsoccershop.comfoxsports-chicago.comfoxsports-newyork.comfoxsports-world.com foxsports.cl foxsports.cofoxsports.co.ve foxsports.comfoxsports.com.arfoxsports.com.bofoxsports.com.brfoxsports.com.cofoxsports.com.ecfoxsports.com.gtfoxsports.com.mxfoxsports.com.pefoxsports.com.pyfoxsports.com.uyfoxsports.com.ve foxsports.gtfoxsports.info foxsports.netfoxsports.net.br foxsports.pe foxsports.sv foxsports.uyfoxsports2.comfoxsportsflorida.comfoxsportsgo.comfoxsportsla.comfoxsportsnetmilwaukee.comfoxsportsneworleans.comfoxsportsracing.comfoxsportssupports.comfoxsportsuniversity.comfoxsportsworld.comfoxstudiolot.com foxsuper6.com -foxtel.com foxtel.com.aufoxtelevisionstations.com  foxtv.com foxtvdvd.com  foxuv.comfoxweatherwatch.com  fssta.com -fxn.ws fxnetwork.comfxnetworks.com bentobox.tv kicu.tv ktvu.commyfoxsanfran.comafewmomentswith.com -anidom.comcasoneexchange.comcoronavirusnow.com -fse.tvgeraldoatlarge.comgooddaychicago.com joeswall.comkilmeadeandfriends.commaskedsingerfox.com -my13la.com -my20dc.commy20houston.com -my29tv.com my45.com  my9nj.commyfoxatlanta.commyfoxaustin.commyfoxboston.commyfoxcharlotte.commyfoxchicago.com myfoxdc.commyfoxdetroit.com myfoxdfw.commyfoxhouston.commyfoxhurricane.com myfoxla.commyfoxlosangeles.commyfoxlubbock.commyfoxmaine.com myfoxny.commyfoxorlando.commyfoxphilly.commyfoxphoenix.commyfoxtampa.commyfoxtampabay.commyfoxtwincities.com myfoxzone.com myq2.comnewsnowfox.comorlandohurricane.comparadisehotelquizfox.com q13.com -q13fox.comrealamericanstories.comrealamericanstories.inforealamericanstories.netrealamericanstories.orgrealamericanstories.tvrealmilwaukeenow.com rprimelab.comshopspeedtv.comsoccermatchpass.comspeeddreamride.comspeedfantasybid.comspeedracegear.com speedxtra.comteenchoice.com testonfox.comtheclasshroom.comthefoxnation.comthegeorgiascene.comwhatthefox.comwhosthehost.com wofl.tvwoflthenewsstation.com wogx.com foxnewsplayer-a.akamaihd.netfreebeacon.comgannett-cdn.com gannett.comgannettdigital.com usatoday.comusatodaynetworkservice.comglobalvoices.orgglobalvoicesonline.org  guo.media  gnews.org gtv.org gtv1.org g-tvapp.com hk01.com hkcnews.com hkopentv.com fantv.hkhongkongfp.comhuffingtonpost.cahuffingtonpost.co.ukhuffingtonpost.co.zahuffingtonpost.comhuffingtonpost.com.auhuffingtonpost.com.mxhuffingtonpost.dehuffingtonpost.eshuffingtonpost.frhuffingtonpost.grhuffingtonpost.inhuffingtonpost.ithuffingtonpost.jphuffingtonpost.kr huffpost.comhuffpostarabi.comhuffpostbrasil.comhuffpostmaghreb.com inmediahk.netbusinessinsider.co.zabusinessinsider.combusinessinsider.com.aubusinessinsider.com.plbusinessinsider.debusinessinsider.esbusinessinsider.frbusinessinsider.inbusinessinsider.jpbusinessinsider.mxbusinessinsider.mybusinessinsider.nlbusinessinsider.sginsider-intelligence.com insider.cominsiderintelligence.com jfengtime.com landofhope.tvlocalpresshk.com -ltn.com.tw yes123.com.tw macrumors.com matters.news matters.one -medium.commedium.systems -myradio.hkmyradio.com.hkdreamworks.com cnbc.com -cnbcfm.com  msnbc.com -nbc.co nbc.com nbcnews.comnbcolympics.com nbcsports.com -nbcuni.comnbcuniversal.com s-nbcnews.com peacocktv.comuniversalorlando.comuniversalpictures.com&"universalpicturesinternational.com!universalstudioshollywood.com newscorp.com dowjones.com dowjones.iodowjoneson.com wsj.com wsj.net barrons.comandysparis.com newsmax.com -newsmax.in newsmaxtv.com newyorker.com -nikkei.com  nikkei.jp nikkei.co.jp now.com -now.com.hk nowe.com -now-tv.comnow-ashare.com nyt.com nyt.net nytcn.me  nytco.com nytimes.com nytstyle.comtimestalks.com aweencore.com  awetv.comherringnetwork.com oanencore.com oann.compassiontimes.hk qmap.pubrealvision.com reuters.com -reuters.tvreutersmedia.net rfa.org -rfi.fr rsf.org -rt.com etvonline.hk rthk.hk -rtings.com scmp.com -i-scmp.comshuziyimin.orgsinoinsider.com slashdot.orgsky sky.com skyassets.comskyoceanrescue.comskyoceanrescue.deskyoceanrescue.it skysports.com skysports.fr skysports.ieskysportsonline.comskysportsracing.comsoundofhope.org theguardiandev-guardianapis.comdev-theguardian.com -gu-web.netguardianapis.comguardianapps.co.uk -guim.co.uktheguardian.comtheguardiandns.comtheinitium.com  init.shopthenationalpulse.comthestandnews.comdreamteamfc.com  page3.comsunbingo.co.uk talksport.comthescottishsun.co.uk thesun.co.uk  thesun.ie thetype.cloud -cn thetype.comtypeisbeautiful.com udn.com -udn.com.twudnfunlife.com  unwire.hkamerikaninsesi.comamerikaninsesi.orgamerikaovozi.comamerikayidzayn.comamerikiskhma.com bbg.govdandalinvoa.com darivoa.comdengeamerika.comdengiamerika.comglasamerike.netgolos-ameriki.ru insidevoa.com pashtovoa.com persagg.comradiyoyacuvoa.com urduvoa.com voa.govvoaafaanoromoo.comvoaafrique.comvoabambara.com voabangla.comvoacambodia.comvoacantonese.comvoachinese.comvoadeewanews.comvoadeewaradio.com voahausa.comvoaindonesia.com voakorea.comvoalingala.comvoandebele.com voanews.com -voanews.euvoanoticias.com voanouvel.comvoaportugues.com voashona.com voasomali.comvoaswahili.com voathai.comvoatibetan.comvoatibetanenglish.comvoatiengviet.com voatour.comvoazimbabwe.comzeriamerikes.com cstatic.net -curbed.com  eater.comfunnyordie.com meridian.net polygon.com -racked.com -recode.net sbnation.com theverge.com vox-cdn.com vox.comvoxcreative.comvoxfieldguide.com voxmedia.com -voxops.net warroom.orgwashingtonpost.com watchout.tw -waa.tw -wenzhao.castartupschool.orgycombinator.com -zaobao.com zaobao.com.sg  zaobao.sg zerohedge.com zoomingin.tv -M - MESSENGERfbmessenger.comm.me messenger.com -nbabot.net -+ - MXPLAYER-ADSevt.mxplay.com -ads -F - -TAGTIC-ADS g1.tagtic.cn -adsxy-log.tagtic.cn -ads -^ - APPLE-ADSiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -ads - -MPV -mpv.io - -NIKEnikeair-nike-shoes.com airmax360.combeijingnike.combetternike.combuynikechina.comcheap-nike.comcheapnikedunks.comcheapnikeoutlet.comclassicnike.comcnnikebrand.comcustomnikeshoes.com easynike.com -eenike.comfashionnike.com finenike.comfoampositeshoes.comgoldnikeclub.com -gonike.comhandbags-nike.comhightopnikes.com -idnike.cominikesneakers.com just-do-it.cn -cn justdoit.uskicksnike1.comkidsnikeshoes.com -mynike.comnike-air-max.comnike-dunksb.com nike-fr.com nike-org.com nike-shox.cn -cn nike-uk.com nike-us.com nike-usa.com nike.cinike.cn -cn nike.com nike.com.br nike.com.hk nike.gy nike.hk  nike.shop nike.usnike.xn--hxt814e nike0594.com -nike23.comnikeadidas.comnikeairhuarache.comnikeairmax.comnikeairmaxs.comnikeairmaxshoes.comnikeairrift.com nikeasia.comnikeaustralia.com nikeaw77.com  nikeb.com nikebbn.comnikebetrue.comnikebetterworld.cn -cnnikebetterworld.com"nikebetterworld.com.cn -cnnikebetterworld.infonikebetterworld.netnikebetterworld.org nikebiz.info nikebuy.com -nikeby.com nikecdn.comnikecompany.comnikeconfluence.com nikechina.cn -cnnikechosen.com nikecloud.com nikeclub.com nikecraft.com nikedawn.com nikedunks.netnikedunksshoes.com nikeelite.comnikefactorystore.com nikefind.com nikefirm.comnikefoamposites.comnikefoampositeshoes.comnikefootballcleats.comnikefootballgloves.comnikefootballjersey.comnikefoundation.org -nikefr.comnikefrance.com nikefree.comnikefreeshoes.comnikefuelband.comnikegadgets.comnikegenealogy.com nikegolf.ca nikegolf.cn -cn nikegrid.com nikehelp.comnikehightops.comnikehyperdunk.com nikeid.cn -cn nikeid.com.cn -cnnikeidshoes.com nikeinc.comnikeinc.com.cn -cnnikeincchemistry.comnikeinstock.com -nikeit.comnikeitalia.com nikeitaly.com -nikekd.com nikekid.com -nikelab.cn -cnnikelab.com.cn -cn nikelink.comnikeliuxiang.cn -cnnikeliuxiang.comnikeliuxiang.com.cn -cn nikelives.comnikelunarglide.com nikemall.cn -cnnikemall.com.cn -cnnikemarketing.com nikenews.com -niken7.comnikeonlinestore.comnikeoutletstore.comnikeoutletstores.comnikepioneer.com nikeplus.com nikeplus.org nikepress.com nikeprice.comnikeproduct.comnikepromax.comnikeresponsibility.comnikereuseashoe.comnikerunclub.cn -cnnikerunclub.com.cn -cnnikerunner.comnikerunning.com.cn -cnnikerunningshoes.com -nikesb.comnikesbdunks.netnikeseason.comnikeselling.comnikesellorder.comnikeshoemarket.comnikeshoes-store.comnikeshoes21.comnikeshoes4u.comnikeshoescity.comnikeshoesgroup.comnikeshoesinc.comnikeshoesmarket.comnikeshoesretro.comnikeshoeswon.comnikeshoponline.comnikeshoxsale.com nikesku.comnikesnowboarding.comnikesoccercleats.comnikesportswear.comnikestares.com nikestore.cn -cn nikestore.comnikestore.com.aunikestyles.com nikeswim.comniketaiwan.netniketracking.comniketradeweb.comniketrainer.comniketrainers.comniketraining.com nikeusa.com nikewear.com  nikey.com nikezoom.com nke6.comoutletnike.com runnike.comrunningnike.comsale-nikeshoes.comstatic-nike.com swoo.sh -swoosh.com  swoosh.tvthedailysnkr.comverygoodnike.comwomensnikeshox.comxn--74q035i.xn--hxt814e - -UN -un.org - -B3LOG  b3log.org -' -HM -hm.com hm.com.cn -cn -A - PINDUODUO -pddpic.com pinduoduo.com yangkeduo.com -% -KUGOU  kgimg.com  kugou.com - - -TYPOGRAPHYtypography.com - -VERCEL -now.shvercel-dns.comvercel-status.com -vercel.app -vercel.comzeit-world.co.ukzeit-world.comzeit-world.netzeit-world.org zeit.co zeit.sh zeitworld.com - -VIDEOJS videojs.com - -AMP amp.devampproject.comampproject.netampproject.orgcdn.ampproject.org -cnamp.akamaized.net - -BBGWATCH bbgwatch.com - - -CITIZENLAB citizenlab.ca -= -LANDIAN -lancdn.comlandiannews.com  wsoso.com - -MOOV moov.hk -Q -WANGSU  8686c.comchinanetcenter.com -wangsu.com ourdvsss.com - -9TO59to5google.com 9to5mac.com9to5terminal.com 9to5toys.com dronedj.com electrek.cospaceexplored.com -7 - BOOTSTRAPbootstrapcdn.comgetbootstrap.com -5 -IMDB imdb.com imdb.tomedia-imdb.com -N -THETYPE thetype.cloud -cn thetype.comtypeisbeautiful.com -s -EHENTAI e-hentai.org ehgt.org ehtracker.org -ehwiki.org exhentai.orghentaiverse.org - -OCULUS binoculus.com buyoculus.com ocul.usoculus-china.com -oculus.comoculus2014.com oculus3d.comoculusblog.comoculusbrand.comoculuscasino.net oculuscdn.comoculusconnect.comoculusdiving.comoculusforbusiness.comoculusrift.com oculusvr.compowersunitedvr.com -{ -QINIUstaticfile.org qbox.me  qiniu.com qiniucdn.com qiniudn.com qiniudns.com  qnssl.com - -OKEX okex.com -- -SURFLITE surflite.co surflite.net - -AMYTELE amytele.com - -CDNJS  cdnjs.com -" - LETSENCRYPTletsencrypt.org -/ -ADGUARDadguard-vpn.com adguard.com - -MAYING  maying.co - - SPANKBANG spankbang.com - -CATEGORY-SCHOLAR-CN -csdl.ac.cn  las.ac.cn cnki.netwanfangdata.com.cn  apabi.comcnthinkers.com  cqvip.com - -STAGE1ST saraba1st.com - - -TORPROJECTtorproject.org -' - UBERADS-ADS uberads.com -ads - -APPLEiadsdk.apple.com -ads iad.apple.com -ads qwapi.com -adscalendarserver.orgapple-cloudkit.com cups.orgdeveloper.apple.comdevimages-cdn.apple.comdevstreaming-cdn.apple.comfoundationdb.orgmacosforge.org macruby.com macruby.net macruby.orgapple-mapkit.complaygrounds-cdn.apple.com carekit.orgresearchandcare.orgresearchkit.cn -cnresearchkit.com.cn -cnresearchkit.hkresearchkit.netresearchkit.orgresearchkit.tvservicetalk.io -webkit.org webkitgtk.org wpewebkit.orgwebobjects.com-)appleworldwidedeveloper.hb-api.omtrdc.net)%appleworldwidedeveloper.sc.omtrdc.net#p-events-delivery.akamaized.net100beatscheap.com1monsterbeatsbydreus.com2013beatsbydrdreonline.com2013beatsbydrdreshop.com2013beatsbydreshop.com2013beatshdcybermonday.com2013cheapestbeats.com2013newbeatsworld.com2013pascherbeatsbydre.com2014cheapbeatsbydre.com 2beatsdre.com2buybeatsbydre.com2drdrebeats.com40shopping.com4beatsbydre.com5beatsbydre.comaaagradeheadphones.com -aanaan.comabcheadphones.comabeatsbydrdre.com aeasyshop.com -airmay.com -ajtalk.com albeats.com annstores.netaokwholesale.netaucheapbeats.comaudiobeatsau.comaudiobeatsbydre.comaudiomonsterbeatsonline.comauricularemonsterbeats.com#auricularesbeatsbaratosshop.comauricularesbeatsmarkt.comausbeatsbydrdre.combag-glasses1.com bandgirlz.com baxsound.com bbestmall.com -beatbd.combeatbydre2013.combeatbydremonster.com!beatbydreheadphonesonsale.combeatbydrekopen.combeatbydreuk2014.combeatdrdres.combeatfactoryoutlets.combeatmonstersaustralia.netbeats-by-dre-australia.combeats-bydrdre.netbeats-bydre-mall.combeats-bydrecheapsale.combeats-bydreoutletonline.combeats-bydreoutletsale.combeats-bydreoutletsale.netbeats-bydreoutletssale.netbeats-bydrestore.combeats-bydreuk.combeats-deal.combeats-dre-us.com"beats-headphones-buy-cheap.combeats-headphones.usbeats-sale.combeats-seller.combeats-soaho.com  beats1.cc beats1.cn -cn  beats1.tv beats1.com.cn -cn beats123.com beats2.com.cn -cn beats4.cn -cn -beats4.netbeats4outlets.combeats4salecheap.combeatsallsale.combeatsaudifonos.combeatsaudiobydre.combeatsaudiobydre.netbeatsaudios.netbeatsbeatsmonster.combeatsbestdeals.combeatsblackfridaydeals.netbeatsblackfridayretails.combeatsboxingdayuksale.combeatsbydrdre-headphones.combeatsbydrdre-officials5.combeatsbydrdre-online.combeatsbydrdre-onsale.combeatsbydrdre-store.combeatsbydrdre-store.usbeatsbydrdre4sale.combeatsbydrdrebiz.combeatsbydrdrecustom.combeatsbydrdredanmark.combeatsbydrdremall.combeatsbydrdreoutlet.combeatsbydrdres.combeatsbydrdresale.netbeatsbydrdrestore.combeatsbydrdreus.com!beatsbydre-beatsheadphone.combeatsbydre-chen.combeatsbydre-club.combeatsbydre-headphones.com!beatsbydre-headphonesshop.combeatsbydre-mall.combeatsbydre-outlet.combeatsbydre-outletsale.netbeatsbydre-outletstore.combeatsbydre-sell.combeatsbydre-store.combeatsbydre-studio.combeatsbydre-us.com beatsbydre.jpbeatsbydre2081.combeatsbydre411.combeatsbydre4usales.combeatsbydreauofficial.combeatsbydreausale.net beatsbydreaustralia-sale.com!beatsbydreaustraliaonline.com"beatsbydreaustraliaonlines.com beatsbydreaustraliasales.combeatsbydrebeatsby.com!beatsbydreblackfriday2013.com beatsbydreblackfridaypro.combeatsbydreboxingdayca.combeatsbydrecasquesfr.com#beatsbydrecheap-outletstore.combeatsbydrecheaper.com"beatsbydrecheaponlinesales.combeatsbydrecolors.com beatsbydrecustomwireless.com&"beatsbydrecybermondaydeals2013.com&"beatsbydrecybermondaydeals2013.netbeatsbydredanmarks.com"beatsbydredealsblackfriday.com"beatsbydredealscybermonday.combeatsbydrediscount.com beatsbydrediscountonline.netbeatsbydredr.combeatsbydreexecutivesale.com beatsbydreformall2013-nl.com beatsbydreforsalesonline.com beatsbydreforshop2013-nl.combeatsbydreforstore.combeatsbydreforyououtlet.combeatsbydrefr.com!beatsbydrefrcasquepascher.combeatsbydrefriday.combeatsbydregot.combeatsbydrehd.combeatsbydrehd.netbeatsbydreheadphones-nz.combeatsbydreheadphonesolo.combeatsbydrehut.combeatsbydreinexpensive.combeatsbydreireland-sales.com beatsbydreirelandonlines.combeatsbydreirelandsale.com beatsbydreirelandsonline.combeatsbydremall2013.com"beatsbydremonsteraustralia.combeatsbydrenls.combeatsbydrenorge1.net!beatsbydreofficialdanmark.combeatsbydreoksale.combeatsbydreol.combeatsbydreonlie2013-nl.com!beatsbydreonlines-ireland.combeatsbydreonlines-uk.combeatsbydreonlinesale-nz.combeatsbydreoordopjes.combeatsbydreoslo.combeatsbydreoutletsale.combeatsbydreoutletscheap.combeatsbydrerealstore.combeatsbydres-shop.combeatsbydresale-uk.combeatsbydresalemall2013.combeatsbydresales.us'#beatsbydresalesonline-australia.com beatsbydresaleonlines-nz.combeatsbydresdanmark.netbeatsbydreshop-uk.combeatsbydreshops.netbeatsbydresingapores.combeatsbydresingaporesale.combeatsbydrespeakers.combeatsbydresold.com%!beatsbydresolohdonline-canada.combeatsbydresonline-nz.combeatsbydrestorevip.combeatsbydressale.combeatsbydressolo.combeatsbydresstudio.com"beatsbydrestudio-australia.combeatsbydrestudio.combeatsbydretoutlet.combeatsbydreuk.combeatsbydreuk.netbeatsbydreus.combeatsbydrevipde.combeatsbydrew.combeatsbydrsmonsterinusa.combeatsbymusic.netbeatsbysdrbre.combeatsbysdrdres.combeatscasque-france.combeatscheap-nz.combeatscheap-uk.combeatscheapforsale.combeatscollection2014.combeatscustomblackfriday.combeatsdanmark2013.combeatsdrdre-headphones.combeatsdrdre-it.combeatsdrdre-solo.combeatsdrdre2014.combeatsdrdrecuffie.netbeatsdrdrekaufenschweiz.netbeatsdrdreneon.combeatsdre-monster.com beatsdre.netbeatsdre4cheap.combeatsdrecheap.combeatsdredreheadphones.combeatsdreforsale.combeatsdreinau.combeatsdremonster-uk.com!beatsdrenewcolorful4usale.combeatsdreoutletsale.combeatsdresale2013.combeatsdresalestore.combeatsdresolo2013.combeatsdreus.combeatsearbudsheadphoness.combeatsfacstore.combeatsfactoroutlets.combeatsfactorycollection.combeatsfactoryoutles.combeatsforcheap-usa.combeatsforme.combeatsfranceofficiel.combeatselectronic.netbeatselectronics.com -beatsep.cn -cn beatsep.com beatsep.net beatsheadphones-discount.combeatsheadphones1.combeatsheadphones2u.combeatsheadphonesale.combeatsheadphonesdealer.combeatsheadphonesforcheap.netbeatsheadphonesonline.combeatsheadphoness.combeatsheadphonestudio.combeatsheadphonesusamall.combestshoesale2014.netbeatshopuk.combeatsincanada.combeatsinsingapore.combeatsireland.net beatsjoy.combeatsmonstersales.combeatsmusic.combeatsmusic.wang beatsneon.combeatsnzsale.com beatsodre.combeatsofdre-usa.combeatsonblackfriday2013.com beatsone.netbeatsoutlet.netbeatsoutletanytime.combeatsoutletonlines.combeatspascher-bydre.combeatspascher-fr.netbeatspascher.netbeatspaschers.netbeatspillnewcolor.combeatspills.combeatspromonsterjp.combeatspromotions.combeatssales.combeatssaleus.combeatssaustraliabuy.combeatssbyaustralia.combeatssbydredanmark.combeatsshop-uk.combeatsshop-usa.combeatsshopstore.combeatssingapores.combeatssingaporeshop.com beatsstudiohodetelefoner.combeatsstudiosite.combeatstoreusa.com beatsua.combeatswholesale.usbeatswirelesscheap.combeatswirelesscuffie.combegin-trade.combestbeats4u.com -biitii.comblackfridaydrebeatsnew.comblackfridaydrebeatsshop.combloommicroventures.com bookonsky.net -bossae.combrandeasygo.combrandelectronic.combrandproducts1688.com brands098.combusinesswebwise.combuy-from-shanghai.combuybeatsbydre-uk.combuybeatsheadphonesbydre.combuycheapbeatsbus.combuycheapbeatsbydre.combuycheapbeatsbydreshop.combuycheapbeatsdreuk.combuydrdrebeatbox.combuyminibeatbox.combuyonlineheadphones.combuyshoponly.combuzzardflapper.combybeatsdre.combydrebeats.comcanada-beatsbydre.com casque-fr.com casquebeatsbydrdresolohd.comcasquebeatsdocteurdre.comcasquebeatsdre2013.comcasquebeatsenligne.comcasquebeatsfracheter.comcasquebeatsmer.netcasquebeatsofficiel-fr.comcasquebeatspascher2013.comcasquebeatssolo.netcasquedrdrebeatssfr.comcasquemonsterbeats.com#casquemonsterbeatsbydre2013.comcasquesbeatsaudio.comcheap-beats-by-dre.netcheap-beatsbydre.comcheapbagshoes.com cheapbeats.uscheapbeats365.comcheapbeats4sale.netcheapbeatsaustraliasale.comcheapbeatsbus.comcheapbeatsbydr.comcheapbeatsbydrdrepro.comcheapbeatsbydre-au.comcheapbeatsbydre99.comcheapbeatsbydrefau.comcheapbeatsbydremall.comcheapbeatsbydremonster.comcheapbeatsbydrenz.netcheapbeatsbydreonsale.com cheapbeatsbydreoutlet-nz.comcheapbeatsbydreoutlet.com"cheapbeatsbydreoutlets2013.comcheapbeatsbydresale.comcheapbeatsbydreshop.comcheapbeatsdrdresolo.com$ cheapbeatsbydrestudioedition.com"cheapbeatsbydrestudioutlet.comcheapbeatsdrestudios.comcheapbeatsheadphone2014.comcheapbeatsheadphones.uscheapbeatsie.comcheapbeatsla.comcheapbeatssale4u.comcheapbeatsshopbydre.comcheapcustombeatsbydre.comcheapdrdrebeats8.comcheapdrdrebeatsca.comcheapdrebeats8.netcheaperbeatsbydresale.comcheapestbeatsdrdre.comcheapheadphonesland.comcheapheadphonessale.comcheapheadsetmall.comcheapheadsetshop.com cheapmonsterbeatsbydrdre.com!cheapmonsterbeatsheadsets.com cheapmonstersbeatsonsale.comcheapmonsterbeatssale.comcheapmonsterbeatsusa.uscheapnewbeatsbydre.comcheapsalemonster.comcheapshoesvip.comcheapwireless04.comcheapwirelessbeats.comcheerwholesale.uschihair-straightener.comchinaclothesstore.comciattackers.com -cmhalq.com cnshopin.comcoolmonster.netcozydrdrebeats.com cqcorea.comcuffiesaldi.comcustombeatsbydrebuy.comcustombeatsdeals.comcustombeatsforcheap.comcustombeatsny.comcustombeatssbydreus.comcustomdrdrebeats.comcustomizedbeatbydre.comcustomizedbeatsbydre.comcustomizedbeatsdre.comcybermondaybeats4sale.comdanemarket.comdanmarkbeatsbydrdre.comdctbeatsbydre.comdealsbeatsblackfriday.comdetaliczny.comdiscountbeatsbydre-us.comdiscountbeatsstore.comdkbeatsbydre.comdrbeatsukmart.comdrdre-beats.comdrdrebeats-chen.comdrdrebeats-headphone.comdrdrebeats-usa.comdrdrebeatsale.comdrdrebeatsbillig.comdrdrebeatsdesale.comdrdrebeatsdiscount.comdrdrebeatsforu.comdrdrebeatsretail2013.comdrdrebeatssale7.comdrdrebeatsuk.comdrdreheadphonebeats.comdrdreheadphonekey.comdrdreheadphonesusstore.comdrdremonster-beats.comdrdremonsterdre.comdrdreprobeatssale.com drdrefnac.comdrebeats-australia.comdrebeats-france.comdrebeats-monster.com!drebeats-monsteraustralia.comdrebeats-monsterusa.comdrebeats-singapore.comdrebeats-singapore.netdrebeats-singaporecheap.comdrebeats-singaporecheap.netdrebeats-solo.comdrebeats-studio.comdrebeatsaustralia-cheap.comdrebeatsaustralia-cheap.netdrebeatsbuy.comdrebeatsbydreoutlet.comdrebeatscanada.comdrebeatsdeutschland.netdrebeatsforsaleus.comdrebeatsheadphones-nz.comdrebeatshome.comdrebeatsoutletstore.comdrebeatsoldes.comdrebeatspill.comdrebeatssite.com#drebeatssolocybermondaysale.comdrebeatsstudio2013.comdrebeatstudio.com drebyby.comdremonsterbeatsoutlets.comdreprobeats.com dreprofy.com -dtsell.comearphonescheapest.comebuyheadphones.comesbeatsbydrebuy.cometheadphones.comfamosascalvas.comfindmybeats.comfr-beatsbydrestore.comfrancecasquebeatssolde.comfrcasquesbeats.com free-aa.com free-sns.com frishoes.comgamebeforethegame.comgetdrebeatssale.comgmnetworks.net gobeatsye.comgobuyonlinestore.net goodsdunk.netheadphonepubs.comheadphones-cool.comheadphones-dre.com headphones-outlet-online.comheadphonesbeatsaudio.comheadphonesbeatsbydre.comheadphoneses.comheadphoneshome.comheadphoneshotsales.comheadphonesol.comheadphonesretailer.comheadphonessupply.comheadphonezip.comheadset987.comhellokittybeats.com highbolt.net highdefinitionbeatsbydre.com hipaa6020.com hipzoom.nethonawalaan.comhotbeatsonsale.comhotmonsterbeats.comhottestheadphonesonline.com  hrsaz.com ibeats-uk.comibeatsbydre.ccibeatsbydre.comigoshopping.net illianacomputerrecycling.comimonsterbeats.comincentivetravelgifts.comioffergift.com islamiccenterofnewlondon.comitaliabeatsbydrdre.com jkbeats.comkickshatchannel.comkobbeatssbydredk.comkopfhorergunstigshop.comleecountytimes.com lexuemei.comlovebeatsdr.com lovemarca.comluckyissue.com -lyzsxx.com mairbeats.com -makeeu.commallheadphone.commbeats-tech.commbheadphone.com -micstl.commmonsterheadphones.netmonbeats2013.commonster-beats-by-dr-dre.com monster-beats-headphones.commonsterbeats-cheap.commonsterbeats-headphone.commonsterbeats-onsale.commonsterbeats-solo.commonsterbeats-solo.netmonsterbeats365buy.commonsterbeats8beatsbydre.commonsterbeatsale.commonsterbeatsalestore.commonsterbeatssalg.commonsterbeatsau.commonsterbeatsbydrdre-nz.commonsterbeatsbydrdre-usa.com!monsterbeatsbydrdrecanada.com!monsterbeatsbydrdrestudio.commonsterbeatsbydre-nz.commonsterbeatsbydre-usa.commonsterbeatsbydre2015.com'#monsterbeatsbydreaustraliacheap.com!monsterbeatsbydrebilligde.com monsterbeatsbydrefactory.commonsterbeatsbydrenew.commonsterbeatsbydres.commonsterbeatsbydreshop.commonsterbeatscasques.commonsterbeatscommunity.commonsterbeatsdrdrecheap.commonsterbeatsfactory.netmonsterbeatsfinland.commonsterbeatsforsale.commonsterbeatsheadphone.commonsterbeatsheadphones.netmonsterbeatshere.commonsterbeatsitaly.commonsterbeatsmall.commonsterbeatsnegozi.netmonsterbeatsok.com!monsterbeatsonlinestoreuk.commonsterbeatsoutlet.usmonsterbeatspascher.netmonsterbeatsru.commonsterbeatssales.commonsterbeatsshops.netmonsterbeatsstore.com#monsterbeatssydneyaustralia.commonsterbeatstang.commonsterbeatstienda.commonsterbeatstudio.commonsterbydrebeat.com!monstercasquebeatspascher.netmonstercheapbeatss.commonsterdrebeats-canada.netmonsterdrebeats-usa.commonsterdrebeats-usa.netmonsterheadphone.netmonsterproduct.netmonstersbeatbydres.commonstersdebea.commonstershopcheapbeats.netmoreheadphones.com -ms4dre.commybeatsbydrestudio.commybeatsbydreuk.commybeatscheapbydre.commyferrariheadphones.comnamemybeats.com neborder.comnegozimonsterbeats.comnegoziomonsterbeats.comnewbeatsblackfriday.com newbemany.comnewdrediscount.comnewestbeatsbydre.comnewlysprung.netnewmonst1erbeatsto1re.com!newmonsterbeatsheadphones.comnewschristmasshopping.comnflhotsalejerseys.com nicesshop.netofferairjordanlebron.comofficialbeatsbydreshop.comofficialbeatsbydrestore.comofficialdrdre.comofficialheadphone.com ogameblog.comonbeatsbydresale.com onlinemonsterbeatsonsale.com onsalekey.comonsaletrend.comoutletbeatsshop.compascherbeatsmonster.compaulsimon-music.comperfectkickz.netpersoneelsland.compicknicekicks.netpickshoesclothes.com pillbeatsblackfridaysale.compocketbiketrader.compowerbeats2wireless.compowerbeatsbydrdre.compowerbeatsbydre.compromonsterbeatsbydre.comprostudiobeatscybersale.com  rbsgr.comrepsneakermall.com repswing.com!salebeatslasteststyle4you.comsaleblackfridaydrebeats.comsalecheaphandbags.com samebags.com sanvaras.com -scnshop.cc -shoers.com shoestop2.comshop-headphones.netshopbeatsdre.com shopbydre.comshopcustomizedbeats.comshopdrebeats.comshopmonsterbeats.comshopping-days.netshoppinguheadphones.comsneaker666.comsneakerpage.netsneakerskick.comsolostudioksale.com soso7778.comspecialtyheadphones.comspiritclubs.comstraightouttasomewhere.comstudiobeatsbydrdre.comsulwerphoto.comsunglassessale2014.comsuperearsenjoy.comsupplybestjerseys.comsustainthesound.comsverigebeatsbydrdre.com szcheapmonsterheadphones.comszwinnertechnology.com takegoto.comthe-monster-beats.comthebeatsbydre.comthebeatsbydre.netthebeatsheadphonesale.com -theuab.nettopbeatsbydrdreoutlet.comtopbeatsdealer.comtopbeatsforsale.com tradevip1.com tteshop.com ubnw.net -unstyle.us&"usa-beatsbydreheadphonesonsale.net  v-has.com vip-beats.comvipbeatsbydre.comvipheadphones.com vipshoes2.com  wasdj.comwholesale-exporter1.comwholesalediscountpurses.comwholesalefine.comwholesaleonlinemart.comworldcoinpay.comyourmonsterbeats.com%!beatselectronicsllc.tt.omtrdc.netapple-icloud.cn -cnappleicloud.cn -cnapplemx-icloud.comicloud-apple.cn -cnicloud-content.comicloud-isupport.com  icloud.ch -icloud.com icloud.com.cn -cn  icloud.de  icloud.ee  icloud.fi  icloud.fr  icloud.hu  icloud.ie  icloud.is  icloud.jp  icloud.lv icloud.net.cn -cn  icloud.om -icloud.org  icloud.pt  icloud.ro  icloud.se  icloud.si  icloud.sk  icloud.vn icloudads.neticloudapple.cn -cn icloudbox.neticloudbrowser.net icloude.comicloudhome.comicloudmail.neticloudmusic.net icloudnet.net icloudpay.net icloudo.com -icloudo.de icloudo.net icloudos.de icloudos.neticloudsecure.neticloudsetup.comios-icloud.com -me.com myicloud.net mylcloud.net wwwicloud.com!www-cdn.icloud.com.akadns.net!12diasdepresentesdeitunes.com12diasderegalosdeitunes.cl12diasderegalosdeitunes.co!12diasderegalosdeitunes.co.cr!12diasderegalosdeitunes.co.ni!12diasderegalosdeitunes.co.ve12diasderegalosdeitunes.com"12diasderegalosdeitunes.com.co"12diasderegalosdeitunes.com.hn"12diasderegalosdeitunes.com.ni"12diasderegalosdeitunes.com.ve12diasderegalosdeitunes.cr12diasderegalosdeitunes.gt12diasderegalosdeitunes.hn12diasderegalosdeitunes.pe 12joursdecadeauxdeitunes.comapple-itunes.cn -cn itun.es itunbes.com ituneas.comitunes-apple.cn -cnitunes-radio.net  itunes.ca  itunes.co itunes.co.th -itunes.com  itunes.hk  itunes.mx -itunes.org -itunes.rio  itunes.usitunes12days.comitunesapple.cn -cnitunesessentials.comitunesfestivals.comitunesiradio.comituneslatino.comituneslogin.netitunesmatch.comitunesparty.comitunesradio.cn -cnitunesradio.comitunesradio.com.cn -cnitunesradio.rioitunesradio.tvitunesradio.twitunesstore.co itunesu.com itunesu.net iutunes.com wwwitunes.comitunes-apple.com.akadns.netappleswift.com  swift.org -swiftui.cn -cnswiftui.com.cn -cn apple apple.ae apple.at apple.be apple.bg apple.bs apple.ca apple.ch apple.cl apple.cmapple.cn -cn apple.co apple.co.cr apple.co.hu apple.co.jp apple.co.kr apple.co.mz apple.co.nz apple.co.th apple.co.uk  apple.com apple.com.af apple.com.au apple.com.bo apple.com.br apple.com.cn -cn apple.com.co apple.com.de apple.com.do apple.com.gr apple.com.gy apple.com.jm apple.com.lk apple.com.mg apple.com.mx apple.com.my apple.com.pa apple.com.pe apple.com.pl apple.com.pr apple.com.pt apple.com.py apple.com.sg apple.com.sv apple.com.tr apple.com.tw apple.com.uy apple.cz apple.de apple.dk apple.ee apple.es apple.eu apple.fi apple.fr apple.hamburg apple.hn apple.hr apple.hu apple.ie apple.in apple.is apple.it apple.jo apple.jp apple.kr apple.lk apple.lt apple.lv apple.me apple.my  apple.net apple.net.gr apple.nl apple.no apple.pk apple.pl apple.pt apple.ro apple.rs apple.ru apple.sa apple.se apple.sg apple.si apple.sk apple.so apple.tt apple.tw apple.uk apple.usapple.xn--czr694bapple.xn--fiqs8s  apple.xyz aplestore.comapple-store.cn -cnapple-store.netapple-store.wang applestor.com applestore.bg applestore.cc applestore.ch applestore.cm applestore.cn -cnapplestore.co.huapplestore.co.jpapplestore.co.ugapplestore.co.ukapplestore.comapplestore.com.auapplestore.com.bnapplestore.com.cn -cnapplestore.com.eeapplestore.com.egapplestore.com.grapplestore.com.hkapplestore.com.hrapplestore.com.joapplestore.com.myapplestore.com.phapplestore.com.plapplestore.com.ptapplestore.com.roapplestore.com.ruapplestore.com.snapplestore.com.tw applestore.de applestore.hk applestore.kr applestore.meapplestore.netapplestore.net.gr applestore.ph applestore.qa applestore.sgapplestore.wangapplestoreonline.comapplestorepro.euonlineapplestore.comapp-store.wangappe-store.comapple-appstore.cn -cnappleappstore.cn -cnappleappstore.netappleappstore.tv  appsto.reappstore.co.id appstore.hk appstore.my appstore.phappstoreapple.cn -cn asto.retvappstore.netapple-imac.com appleimac.comimac-apple.comimac-applecomputer.com imac.co -imac.co.nz imac.com imac.eu imac.gr imac.one imac.rs imacapple.comimacapplecomputer.comebookforipad.com euroipad.comfindmyipad.com indiaipad.com -ipad.co.kr ipad.de  ipad.host  ipad.wang  ipad3.com -ipadair.cl -ipadair.cmipadair.com.bripadair.com.es -ipadair.fr -ipadair.hk -ipadair.ie -ipadair.jp -ipadair.twipadaustralia.com ipadmini.cmipadmini.com.lk ipadmini.lk ipadpro.buzz -ukipad.comappleiphone.huappleiphone.netappleiphonecell.combestiphonestuff.comcooliphonecasesstore.comcustom-iphonecase.comdesigneriphonescases.com dl-iphone.comfundaiphone5s.com hebiphone.com hf-iphone.com -iphine.comiphone-8.com.cn -cn iphone-cd.com iphone-cn.com iphone-sh.comiphone-vip1.comiphone-vip2.comiphone-vip3.comiphone-vip4.comiphone-vip5.com iphone-x.tv iphone-yh.com iphone-zh.com  iphone.cm -iphone.com iphone.com.au iphone.com.gr  iphone.es iphone.host iphone.net.gr  iphone.pt  iphone.rs  iphone.ru iphone.wang iphone4.comiphone4.com.br iphone4s.com iphone5.comiphone5casescovers.comiphone5s5case.comiphoneacessorios.com.briphonecase2013.comiphonecase5.comiphonecases100.comiphonecases5.comiphoneclaro.com.briphonecollcase.com iphonefc.comiphonegermany.comiphonehangzhou.comiphoneimessage.comiphoneplus.wang iphonerip.net iphonese.tviphoneunlockimei.com iphonexs.tvonlyiphone5case.comaplleipods.comappleclassicipod.comdownloadsforipod.com ipod.ca ipod.ch ipod.cm ipod.co -ipod.co.nz -ipod.co.uk -ipod.co.za ipod.com ipod.com.au ipod.com.cn -cn ipod.com.fr ipod.com.hk ipod.com.sg ipod.com.tw ipod.de ipod.es ipod.eu ipod.fr ipod.gr ipod.hk ipod.is ipod.net ipod.no ipod.pk ipod.rs ipod.ru ipod.tw ipodcentre.nlipodcleaner.comipoditouch.com ipodnano.com ipodnano.netipodprices.com -ipodrip.caipodrocks.com.au  ipods.comipodshop.com.au ipodtouch.co ipodtouch.com -myipod.netoffrezdesipods.comsimplyipod.comwwwipodlounge.comimacsources.comlaunchpad.wang mac.wang -macbook.co -macbook.hk -macbook.tw macbook.wang macbookair.cn -cnmacbookair.co.krmacbookair.co.ukmacbookair.commacbookair.com.aumacbookair.com.cn -cnmacbookair.com.es macbookair.es macbookair.hk macbookair.kr macbookair.jpmacbookair.net macbookpro.comacbookpro.commacbookpro.com.aumacbookpro.net macbookpro.usmacbookpros.commacbooksale.comwwwmacbookair.comapple-watch.com.ru applewatch.hk applewatch.tv applewatch.twapplewatch.wangapplewatchedition.comapplewatchseries3.netapplewatchsport.comapplemusic.berlinapplemusic.comapplemusic.com.auapplemusic.hamburgapplemusic.wangapplemusicconnect.comapplemusicfestival.com musickit.netwwwapplemusic.comapple.comscoreresearch.com -apple.newsappleenews.comapplenews.berlinapplenews.hamburg applenews.tvapplenewsformat.com apple-pay.com apple-pay.rsapple-pay.wangapplepay.berlinapplepay.co.rsapplepay.com.twapplepay.hamburg applepay.hk applepay.info applepay.jp applepay.rs applepay.tvapplepaycash.cn -cnapplepaycash.com.cn -cnapplepaycash.netapplepaycash.tv!applepaymerchantsupplies.infoapplepaysupplies.berlinapplepaysupplies.cn -cnapplepaysupplies.com#applepaysupplies.com.cn -cnapplepaysupplies.infoapplepaysupplies.netapplepaysupplies.tvapplewallet.comapplewallet.tv -i-book.com -i-book.net ibook.co.nz  ibook.com ibook.eu  ibook.netibookpartner.comibooksauthor.com  iphoto.eu  iphoto.no  iphoto.se iphoto.wang  imovie.euimoviegallery.comimoviestage.comapplecare.berlin applecare.cc applecare.euapplecare.hamburgapplecare.wangappledarwin.comappledarwin.netdarwinsource.comdarwinsource.orgdarwinsourcecode.comappleaccount.netappleid-applemx.comappleid-applemx.usappleid-iclou.com appleid-uk.usappleid.berlin appleid.comappleid.hamburg -appleid.hk ids-apple.com myappleid.com appletv.com -appletv.fr appletv.wang appletv4.cn -cnappletv4.com.cn -cnappletvapp.appleappleone.audio appleone.blog appleone.chatappleone.cloud appleone.clubappleone.community appleone.filmappleone.guide appleone.hostappleone.space appleone.techappleone.website -shazam.com1to1computing.com.au1to1conference.com.au 5lml.com -a0pple.net aapl.twairport.brussels airport.com -airport.euairsupportapp.com airtunes.com airtunes.info airtunes.netalchemysynth.com  ap0le.com aple.com  apole.com -app0le.com -appl-e.com appl.com -appl4e.com  appla.comapple-darwin.comapple-darwin.netapple-darwin.org apple-dns.cn -cn apple-dns.comapple-dns.com.cn -cn apple-dns.netapple-enews.comapple-expo.com apple-expo.eu apple-hk.comapple-ibooks.cn -cn apple-inc.netapple-livephotoskit.com apple-maps.cn -cnapple-online.com apple-usa.netappleafrica.comappleantilles.comappleaustralia.comappleaustralia.com.auappleaustralia.net.au applebk.netapplecarbon.com applecard.tvapplecentar.co.rsapplecentar.rsapplecenter.cn -cnapplecenter.com.cn -cnapplecentre.com.auapplecentre.infoappleclub.com.hk applecom.comapplecomputer-imac.comapplecomputer.cn -cnapplecomputer.co.inapplecomputer.co.nzapplecomputer.com applecomputer.com.cn -cnapplecomputer.com.hkapplecomputer.com.twapplecomputer.huapplecomputer.krapplecomputerimac.comapplecomputerinc.infoapplecomputers.co.nzapplecoronavirus.com appledns.cn -cnappledns.com.cn -cn appleexpo.euappleexpo.infoapplefilmaker.comapplefilmmaker.comapplefinalcutproworld.comapplefinalcutproworld.netapplefinalcutproworld.orgapplehongkong.comapplehongkong.com.hkappleinclegal.comappleipodsettlement.comappleiservices.com applelink.comapplemagickeyboard.comapplemalaysia.com.myapplemasters.infoappleonline.comappleonline.net appleoriginalproductions.com -appleos.tvapplepencil.netapplepodcasts.comapplepremiumreseller.com.au applepremiumresellers.com.auapplereach.comapplereach.net apples-msk.ruapplescreensavers.comapplescript.infoappleshare.infoappleshop.co.uk applesiri.cn -cnapplesurveys.comappletaiwan.com appletips.net appleweb.net applexpo.net applezh.com  applf.com -applfe.com -applle.com  applw.comappmediagroup.com -appule.com  appye.com  apyle.com  beddit.tvbraeburncapital.com burstly.netburstlyrewards.com buyaapl.com buyaapl.net buyaple.com camelphat.comcamelspaceeffect.com -carbon.com catalina.hk cdn-apple.com  chomp.com -coreml.netdashwood360.comdesktopmovie.comdesktopmovie.netdesktopmovie.orgdesktopmovies.netdesktopmovies.orgdeveloper-advisor.comdevice-manager.usdigitalhub.comdiscussionsapple.com -dot-mac.de  dotmac.dedvdstudiopro.bizdvdstudiopro.comdvdstudiopro.infodvdstudiopro.netdvdstudiopro.orgdvdstudiopro.us -earpod.net ecgapp.com.cn -cn -ecgapp.netedu-research.org -emac.co.in emac.in  emagic.de -eworld.com faceid99.com faceid99.netfaceidglobal.com faceshift.cn -cn facetime.netfinalcutpro.comfind-apple.com firewire.cl firewire.eu geoport.com -homepod.cn -cn -hopstop.tv ichat.co.in idvd.eu  ikids.comilecture.co.nz ilife.eu ilife.gr -ilife.wang imessage.tv insidear.cn -cninsidemacintosh.comiosinthecar.comipa-iphone.net  ipple.com ischool.com itools.info  iwork.com iwork.se -iwork.wangjetfuelapp.comjetfuelapps.comlatticedata.com livephotos.cn -cnlivephotos.com.cn -cn livephotos.tvlojaiphone.com.br mac-mini.com mac.com -mac.com.au -mac.eu -mac.rs macappsto.re macboxset.com mach-os.com mach-os.net -machos.net macintosh.eumacintoshsoftware.com macmini.com macos.com.aumacossierra.com macosx.infomacosxlion.com macpazar.com macreach.com macreach.net mobileme.dkmr-apple.com.tw mzstatic.com -newton.com next.com overapple.com powerbook.eupullstring.netquatrowireless.com quicktime.cc quicktime.comquicktime.com.au quicktime.eu quicktime.net quicktime.tvreform-apple.comservices-apple.net siri.com spektral.cc texture.comthinkdifferent.us -touchid.tv touchid.wangtracking-location.comtruedepth3d.com tryrating.comwebobjects.co.uk webobjects.de webobjects.euwebobjects.net wuxiapple.com wwapple.netwww-sms-apple.com wwwapple.com wwwapple.net wwwlapple.comxn--czrs0t4phtr3a.cnxn--fiqs8sxootzz.cn xn--fiqs8sxootzz.xn--hxt814e!xn--gtvq61aiijy0b.xn--hxt814exn--gtvz22d.wangxn--gtvz22d.xn--hxt814exn--hxtr4rozx.xn--czr694b xn--kput3imi374g.xn--hxt814e#xn--ohq11k7pl25iyo8a.cn -cn xn--ruq8a011kt6y.xn--hxt814e, adcdownload.apple.com.akadns.net -cn!adcdownload.apple.com -cn."amp-api-search-edge.apps.apple.com -cn"amp-api.apps.apple.com -cn aod.itunes.apple.com -cn#api-edge.apps.apple.com -cnappldnld.apple.com -cn"appldnld.g.aaplimg.com -cnapps.apple.com -cnapps.mzstatic.com -cn(apptrailers.itunes.apple.com -cn bag.itunes.apple.com -cn'bookkeeper.itunes.apple.com -cnbooks.apple.com -cn!cds-cdn.v.aaplimg.com -cn$cds.apple.com.akadns.net -cn cds.apple.com -cncl2-cn.apple.com -cn<0cl2.apple.com.edgekey.net.globalredir.akadns.net -cncl4-cn.apple.com -cn'client-api.itunes.apple.com -cn+clientflow.apple.com.akadns.net -cn clientflow.apple.com -cn cma.itunes.apple.com -cn."configuration.apple.com.akadns.net -cn#configuration.apple.com -cn!communities.apple.com -cncstat.apple.com -cn."dd-cdn.origin-apple.com.akadns.net -cn(discussionschinese.apple.com -cn(download.developer.apple.com -cnfides-pol.apple.com -cngs-loc-cn.apple.com -cngs-loc.apple.com -cn%gsp10-ssl-cn.ls.apple.com -cn!gsp11-cn.ls.apple.com -cn!gsp12-cn.ls.apple.com -cn!gsp13-cn.ls.apple.com -cnC7gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net -cn, gsp4-cn.ls.apple.com.edgekey.net -cn gsp4-cn.ls.apple.com -cn gsp5-cn.ls.apple.com -cn(gspe11-2-cn-ssl.ls.apple.com -cn&gspe12-cn-ssl.ls.apple.com -cn&gspe19-cn-ssl.ls.apple.com -cn-!gspe19-cn.ls-apple.com.akadns.net -cn"gspe19-cn.ls.apple.com -cn#gspe21-ssl.ls.apple.com -cngspe21.ls.apple.com -cn#gspe35-ssl.ls.apple.com -cn&gspe85-cn-ssl.ls.apple.com -cniadsdk.apple.com -cn, icloud-cdn.icloud.com.akadns.net -cn icloud.cdn-apple.com -cn'images.apple.com.akadns.net -cn?3images.apple.com.edgekey.net.globalredir.akadns.net -cnimages.apple.com -cn3'init-p01md-lb.push-apple.com.akadns.net -cn init-p01md.apple.com -cn3'init-p01st-lb.push-apple.com.akadns.net -cn%init-p01st.push.apple.com -cn3'init-s01st-lb.push-apple.com.akadns.net -cn%init-s01st.push.apple.com -cn!init.itunes.apple.com -cn$iosapps.itunes.apple.com -cn(iosapps.itunes.g.aaplimg.com -cniphone-ld.apple.com -cn'itunes-apple.com.akadns.net -cnitunes.apple.com -cn#itunesconnect.apple.com -cn"js-cdn.music.apple.com -cn km.support.apple.com -cnmaps.apple.com -cn)mesu-cdn.apple.com.akadns.net -cn+mesu-china.apple.com.akadns.net -cnmesu.apple.com -cnmusic.apple.com -cn(ocsp-lb.apple.com.akadns.net -cnocsp.apple.com -cnoscdn.apple.com -cn-!oscdn.origin-apple.com.akadns.net -cn$osxapps.itunes.apple.com -cnpancake.apple.com -cn, pancake.cdn-apple.com.akadns.net -cnpd.itunes.apple.com -cnphobos.apple.com -cn!play.itunes.apple.com -cn1%prod-support.apple-support.akadns.net -cn$se-edge.itunes.apple.com -cn se2.itunes.apple.com -cn#search.itunes.apple.com -cn1%sf-api-token-service.itunes.apple.com -cnsp.itunes.apple.com -cn4(stocks-sparkline-lb.apple.com.akadns.net -cn&stocks-sparkline.apple.com -cn>2store.apple.com.edgekey.net.globalredir.akadns.net -cn'store.apple.com.edgekey.net -cnstore.apple.com -cn2&store.storeimages.apple.com.akadns.net -cn+store.storeimages.cdn-apple.com -cn+streamingaudio.itunes.apple.com -cnsu.itunes.apple.com -cn2&support-china.apple-support.akadns.net -cnsupport.apple.com -cn."swcatalog-cdn.apple.com.akadns.net -cnswcatalog.apple.com -cnswcdn.apple.com -cnswcdn.g.aaplimg.com -cn'swdist.apple.com.akadns.net -cnswdist.apple.com -cn+swscan-cdn.apple.com.akadns.net -cnswscan.apple.com -cn!sync.itunes.apple.com -cn1%updates-http.cdn-apple.com.akadns.net -cn&updates-http.cdn-apple.com -cn upp.itunes.apple.com -cnvalid.apple.com -cn-!valid.origin-apple.com.akadns.net -cn"weather-data.apple.com -cn<0www.apple.com.edgekey.net.globalredir.akadns.net -cn%www.apple.com.edgekey.net -cn www.apple.com -cn#^a[1-5]\.mzstatic\.com$ -cn0$^cdn(-cn)?[1-4]?\.apple-mapkit\.com$ -cn9-^cl[1-5]-cdn\.origin-apple\.com\.akadns\.net$ -cn!^cl[1-5]\.apple\.com$ -cn+^is[1-5](-ssl)?\.mzstatic\.com$ -cn$^s[1-5]?\.mzstatic\.com$ -cnapple.com.akadns.net%!courier-push-apple.com.akadns.netpush-apple.com.akadns.net - -JAVHD  javhd.com - - STARBUCKS starbucks.castarbucks.co.uk starbucks.comstarbucks.com.cn -cn starbucks.de starbucks.fr starbucks.ie - -SOLIDOT solidot.org -R -UPAI  aicdn.com upai.com upaiyun.com  upcdn.net  upyun.com -: -ZAOBAO -zaobao.com zaobao.com.sg  zaobao.sg - -KINGSOFT cmcm.com cmcmcdn.com  liebao.cn duba.com duba.net ijinshan.comseasungames.com xishanju.com xoyo.com testplus.cn -wps.cn  51dzt.com  iciba.com  docer.com kdocs.cn papocket.com  wpspdf.cn kimg.cn kingsoft.com ksmobile.com -ks-cdn.com  ksyun.com ksyungslb.com -( -LAGOU  lagou.com lgstatic.com - -OPENVPN openvpn.net -2 -YNET ynet.cn ynet.com ynet.com.cn - -IPIP ipip.net - -KRAKEN -kraken.com - - -REALVISIONrealvision.com - -RIXCLOUD rixcloud.com -9 - -ARTSTATIONartstation.comartstationmedia.com - -BITSTAMP bitstamp.net -( -GLOBALSECURITYglobalsecurity.org -Q - SOURCEFORGEsourceforge.comsourceforge.net -sf.net fsdn.com -M - -STRIKINGLYmystrikingly.comstrikingly.comstrikinglycdn.com -0 - MIXPANEL-ADS decide.mixpanel.com -ads -f -ROKU fandango.com mgo.commgo-images.com ravm.tv roku.com rokutime.com - -SCOOP scoop.sh -' -HEYZO heydoga.com  heyzo.com - -DMZJ dmzj.com -. -SXL mysxl.cn -sxl.cn  sxlcdn.cn -/ - -THEINITIUMtheinitium.com  init.shop -# -CATEGORY-SCHOLAR-!CN apa.orgpsyccareers.com cambridge.org"cambridgeschoolshakespeare.comcambridgedigital.netcambridgemaths.orgnewisiknowledge.comisiknowledge.com clarivate.comwebofknowledge.comwebofscience.com doi.org doi.info els-cdn.comelsevier-ae.com elsevier.com elsevier.ioengineeringvillage.com  evise.com -scopus.comscholar.google.aescholar.google.atscholar.google.bescholar.google.bgscholar.google.cascholar.google.catscholar.google.chscholar.google.clscholar.google.cnscholar.google.co.crscholar.google.co.idscholar.google.co.ilscholar.google.co.inscholar.google.co.jpscholar.google.co.krscholar.google.co.nzscholar.google.co.thscholar.google.co.ukscholar.google.co.vescholar.google.co.zascholar.google.comscholar.google.com.arscholar.google.com.auscholar.google.com.boscholar.google.com.brscholar.google.com.coscholar.google.com.cuscholar.google.com.doscholar.google.com.ecscholar.google.com.egscholar.google.com.gtscholar.google.com.hkscholar.google.com.lyscholar.google.com.mxscholar.google.com.myscholar.google.com.nischolar.google.com.pascholar.google.com.pescholar.google.com.phscholar.google.com.pkscholar.google.com.prscholar.google.com.pyscholar.google.com.sgscholar.google.com.svscholar.google.com.trscholar.google.com.twscholar.google.com.uascholar.google.com.uyscholar.google.com.vnscholar.google.czscholar.google.descholar.google.dkscholar.google.esscholar.google.fischolar.google.frscholar.google.grscholar.google.hnscholar.google.hrscholar.google.huscholar.google.isscholar.google.itscholar.google.lischolar.google.ltscholar.google.luscholar.google.lvscholar.google.nlscholar.google.noscholar.google.plscholar.google.ptscholar.google.roscholar.google.ruscholar.google.sescholar.google.sischolar.google.sk ic.ac.ukimperial.ac.ukimperialbusiness.schoolimperial.insendi.com -knovel.commit mit.edu mit.netmitpressjournals.orgamamanualofstyle.com anb.org blackstonespoliceservice.com epigeum.com oed.com  omscr.com oup.com -ouplaw.com ox.ac.ukoxfordaasc.comoxfordartonline.comoxfordbibliographies.comoxfordclinicalpsych.com oxforddnb.comoxfordfirstsource.comoxfordhandbooks.comoxfordlawtrove.comoxfordmedicine.comoxfordmusiconline.comoxfordpoliticstrove.com oxfordre.comoxfordreference.comoxfordscholarlyeditions.comoxfordscholarship.comoxfordwesternmusic.comsipriyearbook.org ukwhoswho.com"universitypressscholarship.comveryshortintroductions.comserialssolutions.com proquest.comsciencedirect.comsciencedirectassets.com -apress.com springer.comspringerlink.comspringernature.com wkap.nl tandf.co.uktandfonline.comtaylorandfrancis.comtaylorfrancis.com -aclweb.org acm.org altmetric.comanalytictech.comannualreviews.org  arxiv.org asha.org berkeley.edubiomedcentral.com biorxiv.org  brill.com cell.comcomputingreviews.com degruyter.comdentalhypotheses.comelifesciences.org europepmc.orgfrontiersin.org gale.com galegroup.com  ggsrv.com hindawi.com informs.orgisca-speech.orgjamanetwork.comjapanknowledge.comjbe-platform.com jmlr.org jneurosci.org  jstor.org kuke.comlawdata.com.twliteratumonline.com mdpi.com -nature.com nejm.org neurology.org ovid.com  peerj.comphysiology.org plos.org pnas.org princeton.eduresearchgate.netroutledgehandbooks.comroyalsocietypublishing.org sagepub.comscholarpedia.orgsciencemag.orgscientificamerican.com scitation.orgsilverchair-cdn.comspiedigitallibrary.orgstatsmakemecry.com thelancet.com uchicago.edu ucla.edu  umass.edu  wiley.com yale.edu - -IINA iina.io -( -WASHINGTONPOSTwashingtonpost.com - -TMTPOST tmtpost.com chaindd.com -72whys.com checkzeit.com -tivitv.com tigeek.net.cntmtsmartlife.comtmtsmartrun.com -* -WIWIDE wamawama.com -wiwide.com -< -ANACONDA anaconda.com anaconda.org conda.io -^ -CTEXCEL -ctexcel.ca ctexcel.comctexcel.com.hk -ctexcel.fr -ctexcel.us - -HANYI hellofont.cnhellowebfont.com0,hellofontpreview.oss-cn-beijing.aliyuncs.com-)hellowebfonts.oss-cn-beijing.aliyuncs.com hanyi.com.cnfontstar.com.cn -H -BLENDERbigbuckbunny.org blender.orgelephantsdream.org - -WHO who.int -" -TED ted.com -tedcdn.com - -UBI ubi.com ubisoft.com  uplay.com,(ubisoft-uplay-savegames.s3.amazonaws.com,(ubisoft-orbit-savegames.s3.amazonaws.com)uplaypc-s-ubisoft.cdn.ubi.com -cn - -UDACITY udacity.com - -SVP svp-team.com -: - ENTERMEDIADBentermediadb.netentermediadb.org - - -GOOGLE-ADS2mdn.net -ads admob.com -ads adsense.com -ads$advertisercommunity.com -ads%advertiserscommunity.com -ads"adwords-community.com -ads adwords.com -adsadwordsexpress.com -ads app-measurement.com -ads&clickserver.googleads.com -adsdoubleclick.com -adsdoubleclick.net -ads!google-analytics.com -adsgoogleadapis.com -ads!googleadservices.com -ads googleanalytics.com -adsgoogleoptimize.com -ads"googlesyndication.com -ads!googletagmanager.com -ads"googletagservices.com -ads'googletraveladservices.com -ads -urchin.com -ads>1^adservice\.google\.([a-z]{2}|com?)(\.[a-z]{2})?$ -ads -4 - KHANACADEMYkhanacademy.org kastatic.org - -ROCKSTARrockstargames.com -rsg.sc1-gamedownloads-rockstargames-com.akamaized.net)%media-rockstargames-com.akamaized.net*&videos-rockstargames-com.akamaized.net -" - GETCOMPOSERgetcomposer.org - - PACKAGIST packagist.org -, -PAOFUYUN paofu.cloud paofuyun.me -$ - THESTANDNEWSthestandnews.com -E -WIX wix.com  wixmp.com wixsite.com wixstatic.com - -AZURE azure-dns.comazure-dns.info azure-dns.net azure-dns.orgazure-mobile.netazure-sphere.comazure-test.netazure.cn -cn  azure.com  azure.netazurecomcdn.netazurecontainer.ioazurecosmos.netazurecosmosdb.comazurecosmosdb.infoazurecosmosdb.netazuredatabricks.netazuredevopslaunch.comazuredigitaltwin.comazuredigitaltwins.comazuredigitaltwins.netazuredns-prd.infoazuredns-prd.orgazureedge-test.net azureedge.netazureiotcentral.comazureiotsolutions.comazureiotsuite.comazureplanetscale.infoazureplanetscale.netazureserviceprofiler.comazuresmartspaces.netazurestackvalidation.comazurewebsites.netchinacloudapi.cn -cnchinacloudapp.cn -cnchinacloudsites.cn -cn cosmosdb.info cosmosdb.net devopsms.com gotcosmos.commicrosofteca.commicrosoftiotcentral.comserverlesslibrary.nettryfunctions.comwindowsazure.cn -cnwindowsazure.comazure.microsoft.com"azuremarketplace.microsoft.com - -BIGHEAD bighead.group - -DOGECOIN dogecoin.com - -RSF rsf.org -> -UBUNTU -ubuntu.com -ubuntu.netubuntuforums.org - -TOR onion - -ENFAbebepremium3.com.bo -enfa.co.id enfa.com.vn enfaaplus.com enfababy.com enfabebe.comenfabebe.com.brenfabebe.com.coenfabebe.com.doenfabebe.com.ecenfabebe.com.mxenfabebe.com.peenfabebe.com.veenfabebe3.com.arenfagrow.co.inenfagrow.com.bnenfagrow.com.myenfagrow.com.phenfagrow.com.sg enfagrow4.comenfamama.com.arenfamama.com.ph -enfamil.ca enfamil.com -enfamil.es -enfamil.pl -enfamil.pt enfasmart.comenspireformula.comnutramigen.net nutramigen.pl -O -GUO  guo.media  gnews.org gtv.org gtv1.org g-tvapp.com -) -KEEP gotokeep.com keepcdn.com - -JS-ORG -js.org -" -LIHKG -lih.kg  lihkg.com - - CATEGORY-CASbuypass-ssl.com buypass.com -buypass.no -buypass.se minid.no -comodo.com comodoca.com comodoca2.com comodoca3.com comodoca4.comcybertrust.co.jpcybertrust.ne.jp digicert.com freessl.com geotrust.com rapidssl.com -thawte.com  thawte.de  thawte.fr entrust.netentrustdatacard.comglobalsign-media.com globalsign.be globalsign.chglobalsign.co.ukglobalsign.comglobalsign.com.auglobalsign.com.hkglobalsign.com.sg globalsign.es globalsign.eu globalsign.frglobalsign.net globalsign.nl!secure.globalsign.com -cnocsp.globalsign.com -cn ocsp2.globalsign.com -cn identrust.comidentrust.co.uk identrust.netletsencrypt.org secom.co.jpsecomtrust.net sectigo.comenterprisessl.comhackerguardian.cominstantssl.comoptimumssl.compositivessl.com digital-id.ch digitalid.chmanaged-pki.chmanaged-pki.de pki-post.ch pki-posta.ch pki-poste.chroot-signing.chrootsigning.comssl-certificate.ch -swissid.ch swissign.com swissign.liswisssign-group.chswisssign-group.comswisssign-group.li swisssign.ch swisssign.com swisssign.li swisssign.net swisssign.orgswisssigner.chswisssigner.comswisssigngroup.chswisssigngroup.comswissstick.com swisstsa.ch swisstsa.li trustdoc.ch trustsign.ch zert.ch telekom.com -telekom.de trustwave.com verisign.asia verisign.biz verisign.chverisign.co.inverisign.co.uk verisign.comverisign.com.auverisign.com.brverisign.com.cnverisign.com.esverisign.com.hkverisign.com.sgverisign.com.twverisign.com.vn verisign.de verisign.dk verisign.es verisign.fr verisign.hk verisign.in verisign.info verisign.jobs verisign.mobi verisign.name verisign.net verisign.org verisign.pro verisign.se verisign.sg verisign.tw verisign.us verisign.vnverisigninc.com - - R-PROJECT r-project.org - -HULU -112263.com callhulu.comfindyourlimits.com freehulu.com  hooloo.tv  hoolu.com hoolu.tv hu1u.com huloo.cc huloo.tv hulu.com hulu.tv hulu.ushuluaction.com -huluad.com huluapp.com huluasks.com hulucall.com hulufree.com hulugans.comhulugermany.com -hulugo.com -huluim.comhuluinstantmessenger.com huluitaly.com hulunet.comhulunetwork.com huluplus.comhulupremium.comhulupurchase.com -huluqa.comhulurussia.com huluspain.comhulusports.comhulustream.com huluteam.com -hulutv.com huluusa.comjoinmaidez.com mushymush.tv -myhulu.comoriginalhulu.com payhulu.comregisterhulu.comthehulubraintrust.comwwwhuluplus.com -a -SSRCLOUDchina-internet-exchange.com cxkcloud.com ssrcloud.com ssrcloud.org -= - TRANSLATEWIKItranslatewiki.nettranslatewiki.org - -X-ORG x.org - -AAEX aaex.uk - - DAILYMAIL  anm.co.uk  dailym.aidailymail.co.uk dailymail.comdailymail.com.au dailymail.dkdailymailonline.comdmgmediaprivacy.co.ukmailhealth.commailonline.co.ukmailonline.commailonsunday.co.ukmailonsunday.ie metro.co.uk -mol.imthisismoney.co.uk -thismon.ee -\ -PRIVATE local  localhostlocalhost.sec.qq.comlocalhost.ptlogin2.qq.com - -GOG gog.comgog-statics.comgog.qtlglb.com -cn$gogalaxy.gog-statics.com -cn'menu-static.gog-statics.com -cn'productcard.gog-statics.com -cn(static-login.gog-statics.com -cn'www4-static.gog-statics.com -cn, ^images(-\d)?\.gog-statics\.com$ -cn -' -928PLUS 928.plus -zind.cloud -C -BOOKING -booking.cn -cn booking.com bstatic.com - -GIGABYTE  aorus.com aoruszone.cn -cn gigabyte.cn -cn gigabyte.comgigabyte2.azureedge.netsa78gs.wpc.edgecastcdn.net"sni1dcb6gl.wpc.edgecastcdn.net -$ - CLOUDCONVERTcloudconvert.com -Q - -DUCKDUCKGOduckduckgo.com duck.com -ddg.ggspreadprivacy.com \ No newline at end of file diff --git a/shadowsocks-csharp/Data/i18n.csv b/shadowsocks-csharp/Data/i18n.csv deleted file mode 100644 index fb4e671c0..000000000 --- a/shadowsocks-csharp/Data/i18n.csv +++ /dev/null @@ -1,158 +0,0 @@ -en,ru-RU,zh-CN,zh-TW,ja,ko,fr -#Restart program to apply translation,,,,,, -#This is comment line,,,,,, -#Always keep language name at head of file,,,,,, -#Language name is output in log,,,,,, -"#You can find it by search ""Current language is:""",,,,,, -#Please use UTF-8 with BOM encoding so we can edit it in Excel,,,,,, -,,,,,, -Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks -,,,,,, -#Menu,,,,,, -,,,,,, -System Proxy,Системный прокси-сервер,系统代理,系統代理,システムプロキシ,시스템 프록시,Proxy système -Disable,Отключен,禁用,禁用,無効,비활성화,Désactiver -PAC,Сценарий настройки (PAC),PAC 模式,PAC 模式,PACモード,프록시 자동 구성 (PAC),PAC -Global,Для всей системы,全局模式,全局模式,グローバルプロキシ,전역,Global -Servers,Серверы,服务器,伺服器,サーバー,서버,Serveurs -Edit Servers...,Редактировать серверы…,编辑服务器...,編輯伺服器...,サーバーの編集...,서버 수정…,Éditer serveurs… -Online Config...,,在线配置...,線上配置...,,, -Start on Boot,Автозагрузка,开机启动,開機啟動,システム起動時に実行,시스템 시작 시에 시작하기,Démarrage automatique -Associate ss:// Links,Ассоциированный ss:// Ссылки,关联 ss:// 链接,關聯 ss:// 鏈接,ss:// リンクの関連付け,ss:// 링크 연결, -Forward Proxy...,Прямой прокси…,正向代理设置...,正向 Proxy 設定...,フォワードプロキシの設定...,포워드 프록시,Forward-proxy… -Allow other Devices to connect,Общий доступ к подключению,允许其他设备连入,允許其他裝置連入,他のデバイスからの接続を許可する,다른 기기에서 연결 허용,Autoriser d'autres appareils à se connecter -Local PAC,Локальный PAC,使用本地 PAC,使用本機 PAC,ローカル PAC,로컬 프록시 자동 구성,PAC local -Online PAC,Удаленный PAC,使用在线 PAC,使用線上 PAC,オンライン PAC,온라인 프록시 자동 구성,PAC en ligne -Edit Local PAC File...,Редактировать локальный PAC…,编辑本地 PAC 文件...,編輯本機 PAC 檔案...,ローカル PAC ファイルの編集...,로컬 프록시 자동 구성 파일 수정,Modifier le fichier PAC local ... -Update Local PAC from Geosite,Обновить локальный PAC из Geosite,从 Geosite 更新本地 PAC,從 Geosite 更新本機 PAC,Geosite からローカル PAC を更新,Geosite에서 로컬 프록시 자동 구성 파일 업데이트,Mettre à jour le PAC local à partir de Geosite -Edit User Rule for Geosite...,Редактировать свои правила для Geosite,编辑 Geosite 的用户规则...,編輯 Geosite 的使用者規則...,ユーザールールの編集...,Geosite 사용자 수정,Modifier la règle utilisateur pour Geosite ... -Secure Local PAC,Безопасный URL локального PAC,保护本地 PAC,安全本機 PAC,ローカル PAC を保護,로컬 프록시 자동 구성 파일 암호화,Sécuriser PAC local -Regenerate local PAC on version update,,版本更新后重新生成本地 PAC,版本更新後重新生成本地 PAC,,, -Copy Local PAC URL,Копировать URL локального PAC,复制本地 PAC 网址,複製本機 PAC 網址,ローカル PAC URL をコピー,로컬 프록시 자동 구성 파일 URL 복사,Copier l'URL du PAC local -Share Server Config...,Поделиться конфигурацией сервера…,分享服务器配置...,分享伺服器設定檔...,サーバーの設定を共有...,서버 설정 공유,Partager la configuration du serveur ... -Scan QRCode from Screen...,Сканировать QRCode с экрана…,扫描屏幕上的二维码...,掃描螢幕上的 QR 碼...,画面から QR コードをスキャン...,화면에서 QR코드 스캔,Scanner le QRCode à partir de l'écran ... -Import URL from Clipboard...,Импорт адреса из буфера обмена…,从剪贴板导入URL...,從剪貼簿匯入 URL...,クリップボードから URL をインポート...,클립보드에서 URL 가져오기…,Importer l'URL du presse-papiers ... -Availability Statistics,Статистика доступности,统计可用性,統計可用性,可用性の統計,가용성 통계,Statistiques de disponibilité -Show Logs...,Показать журнал…,显示日志...,顯示記錄檔...,ログの表示...,로그 보기…,Afficher les journaux ... -Verbose Logging,Подробный журнал,详细记录日志,詳細資訊記錄,詳細なログを記録,자세한 로깅 사용,Journalisation détaillée -Updates...,Обновления…,更新...,更新...,アップデート...,업데이트…,Mettre à jour… -Check for Updates...,Проверить обновления…,检查更新,檢查更新,アップデートを確認...,업데이트 확인하기…,Vérifier les mises à jour ... -Check for Updates at Startup,Проверять при запуске,启动时检查更新,啟動時檢查更新,起動時にアップデートを確認,시스템 시작 시 업데이트 확인하기,Vérifier les mises à jour au démarrage -Check Pre-release Version,Проверять предрелизные версии,检查测试版更新,檢查發行前版本更新,ベータ版のアップデートも確認,개발 버전 업데이트 확인하기,Vérifier la version préliminaire -Edit Hotkeys...,Горячие клавиши…,编辑快捷键...,編輯快速鍵...,ホットキーの編集...,단축키 수정…,Modifier les raccourcis clavier ... -About...,О программе…,关于...,關於...,Shadowsocks について...,정보…,A propos -Help,Помощь,帮助,說明,ヘルプ,도움말,Aide -Quit,Выход,退出,結束,終了,종료,Quitter -Edit Servers,Редактирование серверов,编辑服务器,編輯伺服器,サーバーの編集,서버 수정,Éditer serveurs -Load Balance,Балансировка нагрузки,负载均衡,負載平衡,サーバーロードバランス,로드밸런싱,Répartition de charge -High Availability,Высокая доступность,高可用,高可用性,高可用性,고가용성,Haute disponibilité -Show Plugin Output,События плагинов в журнале,显示插件输出,,プラグインの出力情報を表示,플러그인 출력 보이기,Afficher la sortie du plugin -Write translation template,Создать шаблон для перевода,写入翻译模板,,翻訳テンプレートファイルを書き込む,번역 템플릿 쓰기,Écrire un modèle de traduction -,,,,,, -# Config Form,,,,,, -,,,,,, -Statistics configuration,Настройки статистики,统计配置,,統計の設定,통계 설정,Configuration des statistiques -&Add,Добавить,添加(&A),新增 (&A),新規 (&A),추가 (&A),Ajouter -&Delete,Удалить,删除(&D),移除 (&D),削除 (&D),삭제 (&D),Supprimer -Dupli&cate,Дублир-ть,复制(&C),複製 (&C),コピー (&C),복제 (&C),Dupliquer -Server,Сервер,服务器,伺服器,サーバー,서버,Serveur -Server IP,IP-адрес,服务器地址,伺服器位址,サーバーアドレス,서버 IP,Serveur IP -Server Port,Порт,服务器端口,伺服器連接埠,サーバーポート,서버 포트,Port de serveur -Password,Пароль,密码,密碼,パスワード,비밀번호,Mot de passe -Show Password,Показать пароль,显示密码,顯示密碼,パスワードを表示する,비밀번호 보이기,Montrer le mot de passe -Encryption,Шифрование,加密,加密,暗号化,암호화,Chiffrement -Plugin Program,Плагин,插件程序,外掛程式,プラグインプログラム,플러그인 프로그램,Programme de plugin -Plugin Options,Опции плагина,插件选项,外掛程式選項,プラグインのオプション,플러그인 설정,Options de plugin -Need Plugin Argument,Требуются аргументы,需要命令行参数,,コマンドライン引数を有効にする,플러그인 인자가 필요함,Besoin d'un argument de plugin -Plugin Arguments,Аргументы,插件参数,外掛程式參數,プラグインの引数,플러그인 인자,Arguments du plugin -Proxy Port,Порт прокси,代理端口,Proxy 連接埠,プロキシポート,프록시 포트,Port proxy -Portable Mode,Переносимый режим,便携模式,便攜模式,ポータブルモード,포터블 모드,Mode portable -Restart required,Требуется перезапуск программы,需要重新启动SS,需要重新啟動SS,再起動が必要,재시작이 필요합니다,Redémarrage nécessaire -Remarks,Примечания,备注,註解,付記,알림,Remarques -Timeout(Sec),Таймаут(сек),超时(秒),逾時 (秒),タイムアウト (秒),시간 초과 (초),Délai d'attente(sec) -OK,ОК,确定,確定,OK,확인,OK -Cancel,Отмена,取消,取消,キャンセル,취소,Annuler -Apply,Применить,应用,應用,適用,적용,Appliquer -New server,Новый сервер,未配置的服务器,新伺服器,新規サーバー,새 서버,Nouveau serveur -Move &Up,Выше,上移(&U),上移 (&U),上に移動 (&U),위로 (&U),Monter -Move D&own,Ниже,下移(&O),下移 (&O),下に移動 (&O),아래로 (&O),Descendre -deprecated,Устаревшее,不推荐,不推薦,非推奨,더 이상 사용되지 않음,Obsolète -"Encryption method {0} not exist, will replace with {1}",,加密方法{0}不存在,将使用{1}代替,,暗号化方式{0}が存在しません,{1}に置換します,{0} 암호화 방식이 존재하지 않으므로 {1}로 대체될 것입니다.,"Méthode de chiffrement {0} n'existe pas, sera remplacée par {1}" -,,,,,, -# Log Form,,,,,, -,,,,,, -&File,Файл,文件(&F),檔案 (&F),ファイル (&F),파일 (&F),Fichier -&Open Location,Расположение файла,在资源管理器中打开(&O),在檔案總管中開啟 (&O),ファイルの場所を開く (&O),위치 열기 (&O),Ouvrier -E&xit,Выход,退出(&X),結束 (&X),終了 (&X),종료 (&X),Quitter -&View,Вид,视图(&V),檢視 (&V),表示 (&V),보기 (&V),Afficher -&Clear Logs,Очистить журнал,清空日志(&C),清除記錄檔 (&C),ログの削除 (&C),로그 초기화 (&C),Effacer les journaux -Change &Font,Шрифт…,设置字体(&F),變更字型 (&F),フォント (&F),글꼴 변경 (&F),Définir la police -&Wrap Text,Перенос строк,自动换行(&W),自動換行 (&W),右端で折り返す (&W),텍스트 감싸기 (&W),Retour à la ligne -&Top Most,Поверх всех окон,置顶(&T),置頂 (&T),常に最前面に表示 (&T),상단 우선 (Top Most) (&T),Metter en haut -&Show Toolbar,Панель инструментов,显示工具栏(&S),顯示工具列 (&S),ツールバーの表示 (&S),툴바 보여주기 (&S),Afficher la barre d'outils -Log Viewer,Просмотр журнала,日志查看器,記錄檔檢視器,ログビューア,로그 뷰어,Visionneuse de journaux -Inbound,Входящая,入站,輸入,受信,"인바운드 (Rx, 다운로드)",Entrant -Outbound,Исходящая,出站,輸出,送信,"아웃바운드 (Tx, 업로드)",Sortant -,,,,,, -# QRCode Form,,,,,, -,,,,,, -QRCode and URL,QRCode и URL,二维码与 URL,QR 碼與 URL,QR コードと URL,QR코드와 URL,QRCode et URL -,,,,,, -# PAC Url Form,,,,,, -,,,,,, -Edit Online PAC URL,Изменение URL удаленного PAC,编辑在线 PAC 网址,編輯線上 PAC 網址,オンライン PAC URL の編集,온라인 프록시 자동 구성 URL 수정,Modifier l'URL du PAC en ligne -Edit Online PAC URL...,Редактировать URL удаленного PAC…,编辑在线 PAC 网址...,編輯線上 PAC 網址...,オンライン PAC URL の編集...,온라인 프록시 자동 구성 URL 수정…,Modifier l'URL du PAC en ligne ... -Please input PAC Url,Введите URL адрес для PAC-файла,请输入 PAC 网址,請輸入 PAC 網址,PAC URLを入力して下さい,프록시 자동 구성 URL을 입력하세요,Veuillez saisir l'URL PAC -,,,,,, -# Messages,,,,,, -,,,,,, -Shadowsocks Error: {0},Ошибка Shadowsocks: {0},Shadowsocks 错误: {0},Shadowsocks 錯誤: {0},Shadowsocks エラー: {0},Shadowsocks 오류: {0},Erreur shadowsocks: {0} -Port {0} already in use,Порт {0} уже используется,端口 {0} 已被占用,連接埠號碼 {0} 已被使用,ポート番号 {0} は既に使用されています。,{0}번 포트는 이미 사용 중입니다.,Port {0} déjà utilisé -Port {0} is reserved by system,Порт {0} зарезервирован системой,端口 {0} 是系统保留端口,連接埠號碼 {0} 由系統保留, ポート番号 {0} はシステムによって予約されています,{0}번 포트는 시스템에서 사용 중입니다.,Port {0} réservé par le système -Invalid server address,Неверный адрес сервера,非法服务器地址,無效伺服器位址,サーバーアドレスが無効です。,올바르지 않은 서버 주소입니다.,Adresse de serveur non valide -Illegal port number format,Неверный числовой формат порта,非法端口格式,無效連接埠號碼格式,ポート番号のフォーマットが無効です。,올바르지 않은 포트 번호 형식입니다.,Format de numéro de port illégal -Illegal timeout format,Неверный формат таймаута,非法超时格式,無效逾時格式,タイムアウト値のフォーマットが無効です。,올바르지 않은 시간 초과 형식입니다.,Format de délai d'attente illégal -Server IP can not be blank,IP-адрес сервера не может быть пустым,服务器 IP 不能为空,伺服器 IP 不能為空,サーバー IP が指定されていません。,서버 IP는 비어있으면 안됩니다.,L'adresse IP du serveur ne peut pas être vide -Password can not be blank,Пароль не может быть пустым,密码不能为空,密碼不能為空,パスワードが指定されていません。,비밀번호는 비어있으면 안됩니다.,Le mot de passe ne peut pas être vide -Port out of range,Порт выходит за допустимый диапазон,端口超出范围,連接埠號碼超出範圍,ポート番号は範囲外です。,올바른 포트 범위가 아닙니다.,Port hors de portée -Port can't be 8123,Адрес порта 8123 не может быть использован,端口不能为 8123,連接埠號碼不能為 8123,8123 番以外のポート番号を指定して下さい。,8123번 포트는 사용할 수 없습니다.,Le port ne peut pas être 8123 -No update is available,Обновлений не обнаружено,没有可用的更新,沒有可用的更新,お使いのバージョンは最新です。,사용 가능한 업데이트가 없습니다.,Aucune mise à jour n'est disponible -Shadowsocks is here,Shadowsocks находится здесь,Shadowsocks 在这里,Shadowsocks 在這裡,Shadowsocks はここです。,Shadowsocks는 여기에 있습니다,Veuillez trouver Shadowsocks ici -You can turn on/off Shadowsocks in the context menu,Вы можете управлять Shadowsocks из контекстного меню,可以在右键菜单中开关 Shadowsocks,可以在右鍵選項單中開關 Shadowsocks,コンテキストメニューを使って、Shadowsocks を有効または無効にすることができます。,프로그램 메뉴에서 Shadowsocks를 끄고 켤 수 있습니다.,Vous pouvez activer / désactiver Shadowsocks dans le menu contextuel -System Proxy Enabled,Системный прокси включен,系统代理已启用,系統 Proxy 已啟用,システム プロキシが有効です。,시스템 프록시가 활성화되었습니다.,Proxy système activé -System Proxy Disabled,Системный прокси отключен,系统代理未启用,系統 Proxy 未啟用,システム プロキシが無効です。,시스템 프록시가 비활성화되었습니다.,Proxy système désactivé -Failed to update PAC file ,Не удалось обновить PAC файл,更新 PAC 文件失败,更新 PAC 檔案失敗,PAC の更新に失敗しました。,프록시 자동 구성 파일을 업데이트하는데 실패했습니다.,Impossible de mettre à jour le fichier PAC -PAC updated,PAC файл обновлен,更新 PAC 成功,更新 PAC 成功,PAC を更新しました。,프록시 자동 구성 파일이 업데이트되었습니다.,PAC mis à jour -No updates found. Please report to Geosite if you have problems with it.,Обновлений не найдено. Сообщите авторам Geosite если у вас возникли проблемы.,未发现更新。如有问题请提交给 Geosite。,未發現更新。如有問題請報告至 Geosite。,お使いのバージョンは最新です。問題がある場合は、GFWList に報告して下さい。,사용 가능한 업데이트를 찾지 못했습니다. 문제가 있다면 Geosite로 전송해주세요.,Aucune mise à jour trouvée. Veuillez signaler à Geosite si vous avez des problèmes concernant. -No QRCode found. Try to zoom in or move it to the center of the screen.,QRCode не обнаружен. Попробуйте увеличить изображение или переместить его в центр экрана.,未发现二维码,尝试把它放大或移动到靠近屏幕中间的位置,未發現 QR 碼,嘗試把它放大或移動到靠近熒幕中間的位置,QR コードが見つかりませんでした。コードを大きくするか、画面の中央に移動して下さい。,QR코드를 찾을 수 없습니다. 가운데로 화면을 이동시키거나 확대해보세요.,Aucun QRCode trouvé. Essayez de zoomer ou de le déplacer vers le centre de l'écran. -Shadowsocks is already running.,Shadowsocks уже запущен.,Shadowsocks 已经在运行。,Shadowsocks 已經在執行。,Shadowsocks 実行中,Shadowsocks가 이미 실행 중입니다.,Shadowsocks est déjà en cours d'exécution. -Find Shadowsocks icon in your notify tray.,Значок Shadowsocks можно найти в области уведомлений.,请在任务栏里寻找 Shadowsocks 图标。,請在工作列裡尋找 Shadowsocks 圖示。,通知領域には Shadowsocks のアイコンがあります。,트레이에서 Shadowsocks를 찾아주세요.,Trouvez l'icône Shadowsocks dans votre barre de notification. -"If you want to start multiple Shadowsocks, make a copy in another directory.","Если вы хотите запустить несколько копий Shadowsocks одновременно, создайте отдельную папку на каждую копию.",如果想同时启动多个,可以另外复制一份到别的目录。,如果想同時啟動多個,可以另外複製一份至別的目錄。,複数起動したい場合は、プログラムファイルを別のフォルダーにコピーしてから、もう一度実行して下さい。,"만약 여러 개의 Shadowsocks를 사용하고 싶으시다면, 파일을 다른 곳에 복사해주세요.","Si vous souhaitez démarrer plusieurs Shadowsocks, faites une copie dans un autre répertoire." -Invalid QR Code content: {0},,无效二维码内容: {0},無效二維碼內容: {0},,, -Failed to update registry,Не удалось обновить запись в реестре,无法修改注册表,無法修改登錄檔,レジストリの更新に失敗しました。,레지스트리를 업데이트하는데에 실패했습니다.,Impossible de mettre à jour de la base de registre -Import from URL: {0} ?,импортировать из адреса: {0} ?,从URL导入: {0} ?,從URL匯入: {0} ?,{0}:このURLからインポートしますか?,, -Successfully imported from {0},Успешно импортировано из {0},导入成功:{0},導入成功:{0},{0}:インポートしました。,, -Failed to import. Please check if the link is valid.,,导入失败,请检查链接是否有效。,導入失敗,請檢查鏈接是否有效。,インポートに失敗しました。リンクの有効性を確認してください。,, -Warning: importing {0} from a legacy ss:// link. Support for legacy ss:// links will be dropped in version 5. Make sure to update your ss:// links.,,警告: 正在从旧版 ss:// 链接导入 {0}。对旧版 ss:// 链接的支持将于 v5 移除,请及时更新你的链接。,,,, -System Proxy On: ,Системный прокси:,系统代理已启用:,系統 Proxy 已啟用:,システム プロキシが有効:,시스템 프록시 활성화됨: ,Proxy système activé: -Running: Port {0},Запущен на порту {0},正在运行:端口 {0},正在執行:連接埠號碼 {0},実行中:ポート {0},실행 중: 포트 {0}번,En cours d'exécution: port {0} -"Unexpected error, shadowsocks will exit. Please report to","Непредвиденная ошибка, пожалуйста сообщите на",非预期错误,Shadowsocks将退出。请提交此错误到,非預期錯誤,Shadowsocks 將結束。請報告此錯誤至,予想外のエラーが発生したため、Shadowsocks を終了します。詳しくは下記までお問い合わせ下さい:,알 수 없는 오류로 Shadowsocks가 종료될 것입니다. 오류를 여기로 제보해주세요:,Shadowsocks va quitter en présence d/érreur inattendue. Veuillez signaler à -"Unsupported operating system, use Windows Vista at least.","Операционная система не поддерживается, минимальные системные требования: Windows Vista или выше.",不支持的操作系统版本,最低需求为Windows Vista。,不支援的作業系統版本,最低需求為 Windows Vista。,お使いの OS はサポートされていません。Windows Vista 以降の OS で実行して下さい。,지원하지 않는 운영체제입니다. 최소 Windows Vista가 필요합니다.,"Système d'exploitation incompatible, veuillez utiliser Windows Vista ou ultérieure." -"Unsupported .NET Framework, please update to {0} or later.","Версия .NET Framework не поддерживается, минимальные системные требования: {0} или выше.",当前 .NET Framework 版本过低,请升级至{0}或更新版本。,目前 .NET Framework 版本過低,最低需求為{0}。,お使いの .NET Framework はサポートされていません。{0} 以降のバンジョーをインストールして下さい。,지원하지 않는 .NET 프레임워크입니다. {0} 또는 상위 버전으로 업데이트해주세요.,".NET Framework incompatible, veuillez mettre à jour vers {0} ou ultérieure." -Proxy request failed,Не удалось выполнить запрос,代理请求失败,Proxy 要求失敗,プロキシリクエストが失敗しました。,프록시 요청에 실패했습니다.,Échec de la demande de proxy -Proxy handshake failed,Не удалось выполнить хэндшейк,代理握手失败,Proxy 交握失敗,プロキシ ハンドシェイクに失敗しました。,프록시 핸드쉐이크에 실패했습니다.,Échec de la prise de contact par proxy -Register hotkey failed,Не удалось применить настройки горячих клавиш,注册快捷键失败,註冊快速鍵失敗,ホットキーの登錄に失敗しました。,단축키 등록에 실패했습니다.,Échec de l'enregistrement du raccourci clavier -Cannot parse hotkey: {0},Не удалось распознать следующие горячие клавиши: {0},解析快捷键失败: {0},剖析快速鍵失敗: {0},ホットキーを解析できません: {0},단축키를 해석할 수 없습니다: {0},Impossible d'analyser le raccourci clavier: {0} -"Timeout is invalid, it should not exceed {0}",Таймаут не может превышать значение {0},超时无效,不应超过 {0},逾時無效,不應超過 {0},タイムアウト値が無効です。{0} 以下の値を指定して下さい。,올바르지 않은 시간 초과 설정입니다. {0}을 초과하지 않아야 합니다.,"Le délai d'attente invalide, il ne doit pas dépasser {0}" -Cannot find the plugin program file,Файл плагина не найден,找不到插件程序文件,找不到外掛程式文件,プラグインプログラムが見つかりません,플러그인 프로그램 파일을 찾을 수 없습니다.,Impossible de trouver le fichier du programme du plugin -,,,,,, -Operation failure,Операция завершилась неудачей,操作失败,,操作に失敗しました,작업에 실패했습니다.,Operation failure -Auto save failed,Автоматическое сохранение не удалось,自动保存失败,,オートセーブに失敗しました,자동 저장에 실패했습니다.,Échec de l'enregistrement automatique -Whether to discard unconfigured servers,Внесенные изменения будут утеряны,是否丢弃未配置的服务器,,未設定のサーバーを破棄しますか,구성되지 않은 서버 설정 변경 사항을 폐기하시겠습니까?,Eliminer les serveurs non configurés ou non -"Invalid server address, Cannot automatically save or discard changes",Неверный адрес сервера. Невозможно сохранить или отменить изменения,非法服务器地址,无法自动保存,是否丢弃修改,,サーバーアドレスが無効なため、オートセーブできません。変更を破棄しますか,"서버 주소가 올바르지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Adresse de serveur invalide, impossible d'enregistrer ou d'annuler automatiquement les modifications" -"Illegal port number format, Cannot automatically save or discard changes",Неверный числовой адрес порта. Невозможно сохранить или отменить изменения,非法端口格式,无法自动保存,是否丢弃修改,,ポート番号のフォーマットが無効なため、オートセーブできません。変更を破棄しますか,"포트 번호가 올바르지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Format de numéro de port illégal, impossible d'enregistrer ou d'annuler automatiquement les modifications" -"Password can not be blank, Cannot automatically save or discard changes",Пароль не может быть пустым. Невозможно сохранить или отменить изменения,密码不能为空,无法自动保存,是否丢弃修改,,サーバーアドレスが無効なため、オートセーブできません。変更を破棄しますか,"비밀번호를 입력하지 않아 자동 저장 할 수 없습니다, 변경 사항을 폐기하시겠습니까?","Le mot de passe ne peut pas être vide, impossible d'enregistrer ou d'annuler automatiquement les modifications" -"Illegal timeout format, Cannot automatically save or discard changes",Неверный формат таймаута. Невозможно сохранить или отменить изменения,非法超时格式,无法自动保存,是否丢弃修改,,パスワードが指定されていないため、オートセーブできません。変更を破棄しますか,시간 초과 형식이 올바르지 않아 자동 저장 할 수 없습니다. 변경 사항을 폐기하시겠습니까?,"Format de délai d'attente illégal, impossible d'enregistrer ou d'annuler automatiquement les modifications" -,,,,タイムアウト値のフォーマットが無効なため、オートセーブできません。変更を破棄しますか,, -"Error occured when process proxy setting, do you want reset current setting and retry?",Произошла ошибка при обработке настроек. Хотите сбросить текущие настройки и попробовать снова?,处理代理设置时发生错误,是否重置当前代理设置并重试?,,プロキシ設定の処理にエラーが発生しました、現在のプロキシ設定をリセットし、再試行してもいいですか,프록시 설정을 처리하는데에 오류가 발생했습니다. 현재 설정을 폐기하고 다시 시도하시겠습니까?,Une erreur s'est produite lors du processus de configuration du proxy. Voulez-vous réinitialiser le paramètre actuel et réessayer? -"Unrecoverable proxy setting error occured, see log for detail","Произошла серьезная ошибка, подробности можно узнать в журналах",发生不可恢复的代理设置错误,查看日志以取得详情,,プロキシ設定に回復不能なエラーが発生しました、ログで詳細をご確認ください,복구 불가능한 프록시 설정 오류가 발생했습니다. 자세한 정보는 로그를 참조하세요.,"Une erreur de paramètre de proxy irrécupérable s'est produite, consultez le journal pour plus de détails" diff --git a/shadowsocks-csharp/Data/libsscrypto.dll.gz b/shadowsocks-csharp/Data/libsscrypto.dll.gz deleted file mode 100755 index ff39152f8..000000000 Binary files a/shadowsocks-csharp/Data/libsscrypto.dll.gz and /dev/null differ diff --git a/shadowsocks-csharp/Data/privoxy.exe.gz b/shadowsocks-csharp/Data/privoxy.exe.gz deleted file mode 100644 index e6945968d..000000000 Binary files a/shadowsocks-csharp/Data/privoxy.exe.gz and /dev/null differ diff --git a/shadowsocks-csharp/Data/privoxy_conf.txt b/shadowsocks-csharp/Data/privoxy_conf.txt deleted file mode 100644 index bef1c726c..000000000 --- a/shadowsocks-csharp/Data/privoxy_conf.txt +++ /dev/null @@ -1,8 +0,0 @@ -listen-address __PRIVOXY_BIND_IP__:__PRIVOXY_BIND_PORT__ -toggle 0 -logfile ss_privoxy.log -show-on-task-bar 0 -activity-animation 0 -forward-socks5 / __SOCKS_HOST__:__SOCKS_PORT__ . -max-client-connections 2048 -hide-console diff --git a/shadowsocks-csharp/Data/sysproxy.exe.gz b/shadowsocks-csharp/Data/sysproxy.exe.gz deleted file mode 100644 index 980d304b0..000000000 Binary files a/shadowsocks-csharp/Data/sysproxy.exe.gz and /dev/null differ diff --git a/shadowsocks-csharp/Data/sysproxy64.exe.gz b/shadowsocks-csharp/Data/sysproxy64.exe.gz deleted file mode 100644 index c5ff36af3..000000000 Binary files a/shadowsocks-csharp/Data/sysproxy64.exe.gz and /dev/null differ diff --git a/shadowsocks-csharp/Data/user-rule.txt b/shadowsocks-csharp/Data/user-rule.txt deleted file mode 100755 index 8a7fab805..000000000 --- a/shadowsocks-csharp/Data/user-rule.txt +++ /dev/null @@ -1,2 +0,0 @@ -! Put user rules line by line in this file. -! See https://adblockplus.org/en/filter-cheatsheet diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs deleted file mode 100644 index 72dd693af..000000000 --- a/shadowsocks-csharp/Encryption/AEAD/AEADEncryptor.cs +++ /dev/null @@ -1,365 +0,0 @@ -using NLog; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Text; -using Shadowsocks.Encryption.CircularBuffer; -using Shadowsocks.Controller; -using Shadowsocks.Encryption.Exception; - -namespace Shadowsocks.Encryption.AEAD -{ - public abstract class AEADEncryptor - : EncryptorBase - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - // We are using the same saltLen and keyLen - private const string Info = "ss-subkey"; - private static readonly byte[] InfoBytes = Encoding.ASCII.GetBytes(Info); - - // for UDP only - protected static byte[] _udpTmpBuf = new byte[65536]; - - // every connection should create its own buffer - private ByteCircularBuffer _encCircularBuffer = new ByteCircularBuffer(MAX_INPUT_SIZE * 2); - private ByteCircularBuffer _decCircularBuffer = new ByteCircularBuffer(MAX_INPUT_SIZE * 2); - - public const int CHUNK_LEN_BYTES = 2; - public const uint CHUNK_LEN_MASK = 0x3FFFu; - - protected Dictionary ciphers; - - protected string _method; - protected int _cipher; - // internal name in the crypto library - protected string _innerLibName; - protected EncryptorInfo CipherInfo; - protected static byte[] _Masterkey = null; - protected byte[] _sessionKey; - protected int keyLen; - protected int saltLen; - protected int tagLen; - protected int nonceLen; - - protected byte[] _encryptSalt; - protected byte[] _decryptSalt; - - protected object _nonceIncrementLock = new object(); - protected byte[] _encNonce; - protected byte[] _decNonce; - // Is first packet - protected bool _decryptSaltReceived; - protected bool _encryptSaltSent; - - // Is first chunk(tcp request) - protected bool _tcpRequestSent; - - public AEADEncryptor(string method, string password) - : base(method, password) - { - InitEncryptorInfo(method); - InitKey(password); - // Initialize all-zero nonce for each connection - _encNonce = new byte[nonceLen]; - _decNonce = new byte[nonceLen]; - } - - protected abstract Dictionary getCiphers(); - - protected void InitEncryptorInfo(string method) - { - method = method.ToLower(); - _method = method; - ciphers = getCiphers(); - CipherInfo = ciphers[_method]; - _innerLibName = CipherInfo.InnerLibName; - _cipher = CipherInfo.Type; - if (_cipher == 0) { - throw new System.Exception("method not found"); - } - keyLen = CipherInfo.KeySize; - saltLen = CipherInfo.SaltSize; - tagLen = CipherInfo.TagSize; - nonceLen = CipherInfo.NonceSize; - } - - protected void InitKey(string password) - { - byte[] passbuf = Encoding.UTF8.GetBytes(password); - // init master key - if (_Masterkey == null) _Masterkey = new byte[keyLen]; - if (_Masterkey.Length != keyLen) Array.Resize(ref _Masterkey, keyLen); - DeriveKey(passbuf, _Masterkey, keyLen); - // init session key - if (_sessionKey == null) _sessionKey = new byte[keyLen]; - } - - public void DeriveKey(byte[] password, byte[] key, int keylen) - { - byte[] result = new byte[password.Length + MD5_LEN]; - int i = 0; - byte[] md5sum = null; - while (i < keylen) - { - if (i == 0) - { - md5sum = MbedTLS.MD5(password); - } - else - { - Array.Copy(md5sum, 0, result, 0, MD5_LEN); - Array.Copy(password, 0, result, MD5_LEN, password.Length); - md5sum = MbedTLS.MD5(result); - } - Array.Copy(md5sum, 0, key, i, Math.Min(MD5_LEN, keylen - i)); - i += MD5_LEN; - } - } - - public void DeriveSessionKey(byte[] salt, byte[] masterKey, byte[] sessionKey) - { - int ret = MbedTLS.hkdf(salt, saltLen, masterKey, keyLen, InfoBytes, InfoBytes.Length, sessionKey, - keyLen); - if (ret != 0) throw new System.Exception("failed to generate session key"); - } - - protected void IncrementNonce(bool isEncrypt) - { - lock (_nonceIncrementLock) { - Sodium.sodium_increment(isEncrypt ? _encNonce : _decNonce, nonceLen); - } - } - - public virtual void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) - { - if (isEncrypt) { - _encryptSalt = new byte[saltLen]; - Array.Copy(salt, _encryptSalt, saltLen); - } else { - _decryptSalt = new byte[saltLen]; - Array.Copy(salt, _decryptSalt, saltLen); - } - logger.Dump("Salt", salt, saltLen); - } - - public static void randBytes(byte[] buf, int length) { RNG.GetBytes(buf, length); } - - public abstract void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen); - - public abstract void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen); - - #region TCP - - public override void Encrypt(byte[] buf, int length, byte[] outbuf, out int outlength) - { - Debug.Assert(_encCircularBuffer != null, "_encCircularBuffer != null"); - - _encCircularBuffer.Put(buf, 0, length); - outlength = 0; - logger.Trace("---Start Encryption"); - if (! _encryptSaltSent) { - _encryptSaltSent = true; - // Generate salt - byte[] saltBytes = new byte[saltLen]; - randBytes(saltBytes, saltLen); - InitCipher(saltBytes, true, false); - Array.Copy(saltBytes, 0, outbuf, 0, saltLen); - outlength = saltLen; - logger.Trace($"_encryptSaltSent outlength {outlength}"); - } - - if (! _tcpRequestSent) { - _tcpRequestSent = true; - // The first TCP request - int encAddrBufLength; - byte[] encAddrBufBytes = new byte[AddrBufLength + tagLen * 2 + CHUNK_LEN_BYTES]; - byte[] addrBytes = _encCircularBuffer.Get(AddrBufLength); - ChunkEncrypt(addrBytes, AddrBufLength, encAddrBufBytes, out encAddrBufLength); - Debug.Assert(encAddrBufLength == AddrBufLength + tagLen * 2 + CHUNK_LEN_BYTES); - Array.Copy(encAddrBufBytes, 0, outbuf, outlength, encAddrBufLength); - outlength += encAddrBufLength; - logger.Trace($"_tcpRequestSent outlength {outlength}"); - } - - // handle other chunks - while (true) { - uint bufSize = (uint)_encCircularBuffer.Size; - if (bufSize <= 0) return; - var chunklength = (int)Math.Min(bufSize, CHUNK_LEN_MASK); - byte[] chunkBytes = _encCircularBuffer.Get(chunklength); - int encChunkLength; - byte[] encChunkBytes = new byte[chunklength + tagLen * 2 + CHUNK_LEN_BYTES]; - ChunkEncrypt(chunkBytes, chunklength, encChunkBytes, out encChunkLength); - Debug.Assert(encChunkLength == chunklength + tagLen * 2 + CHUNK_LEN_BYTES); - Buffer.BlockCopy(encChunkBytes, 0, outbuf, outlength, encChunkLength); - outlength += encChunkLength; - logger.Trace("chunks enc outlength " + outlength); - // check if we have enough space for outbuf - if (outlength + TCPHandler.ChunkOverheadSize > TCPHandler.BufferSize) { - logger.Trace("enc outbuf almost full, giving up"); - return; - } - bufSize = (uint)_encCircularBuffer.Size; - if (bufSize <= 0) { - logger.Trace("No more data to encrypt, leaving"); - return; - } - } - } - - - public override void Decrypt(byte[] buf, int length, byte[] outbuf, out int outlength) - { - Debug.Assert(_decCircularBuffer != null, "_decCircularBuffer != null"); - int bufSize; - outlength = 0; - // drop all into buffer - _decCircularBuffer.Put(buf, 0, length); - - logger.Trace("---Start Decryption"); - if (! _decryptSaltReceived) { - bufSize = _decCircularBuffer.Size; - // check if we get the leading salt - if (bufSize <= saltLen) { - // need more - return; - } - _decryptSaltReceived = true; - byte[] salt = _decCircularBuffer.Get(saltLen); - InitCipher(salt, false, false); - logger.Trace("get salt len " + saltLen); - } - - // handle chunks - while (true) { - bufSize = _decCircularBuffer.Size; - // check if we have any data - if (bufSize <= 0) { - logger.Trace("No data in _decCircularBuffer"); - return; - } - - // first get chunk length - if (bufSize <= CHUNK_LEN_BYTES + tagLen) { - // so we only have chunk length and its tag? - return; - } - - #region Chunk Decryption - - byte[] encLenBytes = _decCircularBuffer.Peek(CHUNK_LEN_BYTES + tagLen); - uint decChunkLenLength = 0; - byte[] decChunkLenBytes = new byte[CHUNK_LEN_BYTES]; - // try to dec chunk len - cipherDecrypt(encLenBytes, CHUNK_LEN_BYTES + (uint)tagLen, decChunkLenBytes, ref decChunkLenLength); - Debug.Assert(decChunkLenLength == CHUNK_LEN_BYTES); - // finally we get the real chunk len - ushort chunkLen = (ushort) IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(decChunkLenBytes, 0)); - if (chunkLen > CHUNK_LEN_MASK) - { - // we get invalid chunk - logger.Error($"Invalid chunk length: {chunkLen}"); - throw new CryptoErrorException(); - } - logger.Trace("Get the real chunk len:" + chunkLen); - bufSize = _decCircularBuffer.Size; - if (bufSize < CHUNK_LEN_BYTES + tagLen /* we haven't remove them */+ chunkLen + tagLen) { - logger.Trace("No more data to decrypt one chunk"); - return; - } - IncrementNonce(false); - - // we have enough data to decrypt one chunk - // drop chunk len and its tag from buffer - _decCircularBuffer.Skip(CHUNK_LEN_BYTES + tagLen); - byte[] encChunkBytes = _decCircularBuffer.Get(chunkLen + tagLen); - byte[] decChunkBytes = new byte[chunkLen]; - uint decChunkLen = 0; - cipherDecrypt(encChunkBytes, chunkLen + (uint)tagLen, decChunkBytes, ref decChunkLen); - Debug.Assert(decChunkLen == chunkLen); - IncrementNonce(false); - - #endregion - - // output to outbuf - Buffer.BlockCopy(decChunkBytes, 0, outbuf, outlength, (int) decChunkLen); - outlength += (int)decChunkLen; - logger.Trace("aead dec outlength " + outlength); - if (outlength + 100 > TCPHandler.BufferSize) - { - logger.Trace("dec outbuf almost full, giving up"); - return; - } - bufSize = _decCircularBuffer.Size; - // check if we already done all of them - if (bufSize <= 0) { - logger.Trace("No data in _decCircularBuffer, already all done"); - return; - } - } - } - - #endregion - - #region UDP - - public override void EncryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) - { - // Generate salt - randBytes(outbuf, saltLen); - InitCipher(outbuf, true, true); - uint olen = 0; - lock (_udpTmpBuf) { - cipherEncrypt(buf, (uint) length, _udpTmpBuf, ref olen); - Debug.Assert(olen == length + tagLen); - Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, saltLen, (int) olen); - outlength = (int) (saltLen + olen); - } - } - - public override void DecryptUDP(byte[] buf, int length, byte[] outbuf, out int outlength) - { - InitCipher(buf, false, true); - uint olen = 0; - lock (_udpTmpBuf) { - // copy remaining data to first pos - Buffer.BlockCopy(buf, saltLen, buf, 0, length - saltLen); - cipherDecrypt(buf, (uint) (length - saltLen), _udpTmpBuf, ref olen); - Buffer.BlockCopy(_udpTmpBuf, 0, outbuf, 0, (int) olen); - outlength = (int) olen; - } - } - - #endregion - - // we know the plaintext length before encryption, so we can do it in one operation - private void ChunkEncrypt(byte[] plaintext, int plainLen, byte[] ciphertext, out int cipherLen) - { - if (plainLen > CHUNK_LEN_MASK) { - logger.Error("enc chunk too big"); - throw new CryptoErrorException(); - } - - // encrypt len - byte[] encLenBytes = new byte[CHUNK_LEN_BYTES + tagLen]; - uint encChunkLenLength = 0; - byte[] lenbuf = BitConverter.GetBytes((ushort) IPAddress.HostToNetworkOrder((short)plainLen)); - cipherEncrypt(lenbuf, CHUNK_LEN_BYTES, encLenBytes, ref encChunkLenLength); - Debug.Assert(encChunkLenLength == CHUNK_LEN_BYTES + tagLen); - IncrementNonce(true); - - // encrypt corresponding data - byte[] encBytes = new byte[plainLen + tagLen]; - uint encBufLength = 0; - cipherEncrypt(plaintext, (uint) plainLen, encBytes, ref encBufLength); - Debug.Assert(encBufLength == plainLen + tagLen); - IncrementNonce(true); - - // construct outbuf - Array.Copy(encLenBytes, 0, ciphertext, 0, (int) encChunkLenLength); - Buffer.BlockCopy(encBytes, 0, ciphertext, (int) encChunkLenLength, (int) encBufLength); - cipherLen = (int) (encChunkLenLength + encBufLength); - } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs deleted file mode 100644 index 2f73f9e4a..000000000 --- a/shadowsocks-csharp/Encryption/AEAD/AEADMbedTLSEncryptor.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; -using Shadowsocks.Encryption.Exception; - -namespace Shadowsocks.Encryption.AEAD -{ - public class AEADMbedTLSEncryptor - : AEADEncryptor, IDisposable - { - const int CIPHER_AES = 1; - - private IntPtr _encryptCtx = IntPtr.Zero; - private IntPtr _decryptCtx = IntPtr.Zero; - - public AEADMbedTLSEncryptor(string method, string password) - : base(method, password) - { - } - - private static readonly Dictionary _ciphers = new Dictionary - { - {"aes-128-gcm", new EncryptorInfo("AES-128-GCM", 16, 16, 12, 16, CIPHER_AES)}, - {"aes-192-gcm", new EncryptorInfo("AES-192-GCM", 24, 24, 12, 16, CIPHER_AES)}, - {"aes-256-gcm", new EncryptorInfo("AES-256-GCM", 32, 32, 12, 16, CIPHER_AES)}, - }; - - public static List SupportedCiphers() - { - return new List(_ciphers.Keys); - } - - protected override Dictionary getCiphers() - { - return _ciphers; - } - - public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) - { - base.InitCipher(salt, isEncrypt, isUdp); - IntPtr ctx = Marshal.AllocHGlobal(MbedTLS.cipher_get_size_ex()); - if (isEncrypt) - { - _encryptCtx = ctx; - } - else - { - _decryptCtx = ctx; - } - - MbedTLS.cipher_init(ctx); - if (MbedTLS.cipher_setup(ctx, MbedTLS.cipher_info_from_string(_innerLibName)) != 0) - throw new System.Exception("Cannot initialize mbed TLS cipher context"); - - DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt, - _Masterkey, _sessionKey); - CipherSetKey(isEncrypt, _sessionKey); - } - - private void CipherSetKey(bool isEncrypt, byte[] key) - { - IntPtr ctx = isEncrypt ? _encryptCtx : _decryptCtx; - int ret = MbedTLS.cipher_setkey(ctx, key, keyLen * 8, - isEncrypt ? MbedTLS.MBEDTLS_ENCRYPT : MbedTLS.MBEDTLS_DECRYPT); - if (ret != 0) throw new System.Exception("failed to set key"); - ret = MbedTLS.cipher_reset(ctx); - if (ret != 0) throw new System.Exception("failed to finish preparation"); - } - - public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) - { - // buf: all plaintext - // outbuf: ciphertext + tag - int ret; - byte[] tagbuf = new byte[tagLen]; - uint olen = 0; - switch (_cipher) - { - case CIPHER_AES: - ret = MbedTLS.cipher_auth_encrypt(_encryptCtx, - /* nonce */ - _encNonce, (uint) nonceLen, - /* AD */ - IntPtr.Zero, 0, - /* plain */ - plaintext, plen, - /* cipher */ - ciphertext, ref olen, - tagbuf, (uint) tagLen); - if (ret != 0) throw new CryptoErrorException(String.Format("ret is {0}", ret)); - Debug.Assert(olen == plen); - // attach tag to ciphertext - Array.Copy(tagbuf, 0, ciphertext, (int) plen, tagLen); - clen = olen + (uint) tagLen; - break; - default: - throw new System.Exception("not implemented"); - } - } - - public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) - { - // buf: ciphertext + tag - // outbuf: plaintext - int ret; - uint olen = 0; - // split tag - byte[] tagbuf = new byte[tagLen]; - Array.Copy(ciphertext, (int) (clen - tagLen), tagbuf, 0, tagLen); - switch (_cipher) - { - case CIPHER_AES: - ret = MbedTLS.cipher_auth_decrypt(_decryptCtx, - _decNonce, (uint) nonceLen, - IntPtr.Zero, 0, - ciphertext, (uint) (clen - tagLen), - plaintext, ref olen, - tagbuf, (uint) tagLen); - if (ret != 0) throw new CryptoErrorException(String.Format("ret is {0}", ret)); - Debug.Assert(olen == clen - tagLen); - plen = olen; - break; - default: - throw new System.Exception("not implemented"); - } - } - - #region IDisposable - - private bool _disposed; - - // instance based lock - private readonly object _lock = new object(); - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~AEADMbedTLSEncryptor() - { - Dispose(false); - } - - protected virtual void Dispose(bool disposing) - { - lock (_lock) - { - if (_disposed) return; - _disposed = true; - } - - if (disposing) - { - // free managed objects - } - - // free unmanaged objects - if (_encryptCtx != IntPtr.Zero) - { - MbedTLS.cipher_free(_encryptCtx); - Marshal.FreeHGlobal(_encryptCtx); - _encryptCtx = IntPtr.Zero; - } - - if (_decryptCtx != IntPtr.Zero) - { - MbedTLS.cipher_free(_decryptCtx); - Marshal.FreeHGlobal(_decryptCtx); - _decryptCtx = IntPtr.Zero; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADOpenSSLEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADOpenSSLEncryptor.cs deleted file mode 100644 index e122e4b8c..000000000 --- a/shadowsocks-csharp/Encryption/AEAD/AEADOpenSSLEncryptor.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Generic; -using Shadowsocks.Encryption.Exception; - -namespace Shadowsocks.Encryption.AEAD -{ - public class AEADOpenSSLEncryptor - : AEADEncryptor, IDisposable - { - const int CIPHER_AES = 1; - const int CIPHER_CHACHA20IETFPOLY1305 = 2; - - private byte[] _opensslEncSubkey; - private byte[] _opensslDecSubkey; - - private IntPtr _encryptCtx = IntPtr.Zero; - private IntPtr _decryptCtx = IntPtr.Zero; - - private IntPtr _cipherInfoPtr = IntPtr.Zero; - - public AEADOpenSSLEncryptor(string method, string password) - : base(method, password) - { - _opensslEncSubkey = new byte[keyLen]; - _opensslDecSubkey = new byte[keyLen]; - } - - private static readonly Dictionary _ciphers = new Dictionary - { - {"aes-128-gcm", new EncryptorInfo("aes-128-gcm", 16, 16, 12, 16, CIPHER_AES)}, - {"aes-192-gcm", new EncryptorInfo("aes-192-gcm", 24, 24, 12, 16, CIPHER_AES)}, - {"aes-256-gcm", new EncryptorInfo("aes-256-gcm", 32, 32, 12, 16, CIPHER_AES)}, - {"chacha20-ietf-poly1305", new EncryptorInfo("chacha20-poly1305", 32, 32, 12, 16, CIPHER_CHACHA20IETFPOLY1305)} - }; - - public static List SupportedCiphers() - { - return new List(_ciphers.Keys); - } - - protected override Dictionary getCiphers() - { - return _ciphers; - } - - public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) - { - base.InitCipher(salt, isEncrypt, isUdp); - _cipherInfoPtr = OpenSSL.GetCipherInfo(_innerLibName); - if (_cipherInfoPtr == IntPtr.Zero) throw new System.Exception("openssl: cipher not found"); - IntPtr ctx = OpenSSL.EVP_CIPHER_CTX_new(); - if (ctx == IntPtr.Zero) throw new System.Exception("openssl: fail to create ctx"); - - if (isEncrypt) - { - _encryptCtx = ctx; - } - else - { - _decryptCtx = ctx; - } - - DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt, _Masterkey, - isEncrypt ? _opensslEncSubkey : _opensslDecSubkey); - - var ret = OpenSSL.EVP_CipherInit_ex(ctx, _cipherInfoPtr, IntPtr.Zero, null, null, - isEncrypt ? OpenSSL.OPENSSL_ENCRYPT : OpenSSL.OPENSSL_DECRYPT); - if (ret != 1) throw new System.Exception("openssl: fail to init ctx"); - - ret = OpenSSL.EVP_CIPHER_CTX_set_key_length(ctx, keyLen); - if (ret != 1) throw new System.Exception("openssl: fail to set key length"); - - ret = OpenSSL.EVP_CIPHER_CTX_ctrl(ctx, OpenSSL.EVP_CTRL_AEAD_SET_IVLEN, - nonceLen, IntPtr.Zero); - if (ret != 1) throw new System.Exception("openssl: fail to set AEAD nonce length"); - - ret = OpenSSL.EVP_CipherInit_ex(ctx, IntPtr.Zero, IntPtr.Zero, - isEncrypt ? _opensslEncSubkey : _opensslDecSubkey, - null, - isEncrypt ? OpenSSL.OPENSSL_ENCRYPT : OpenSSL.OPENSSL_DECRYPT); - if (ret != 1) throw new System.Exception("openssl: cannot set key"); - OpenSSL.EVP_CIPHER_CTX_set_padding(ctx, 0); - } - - public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) - { - OpenSSL.SetCtxNonce(_encryptCtx, _encNonce, true); - // buf: all plaintext - // outbuf: ciphertext + tag - int ret; - int tmpLen = 0; - clen = 0; - var tagBuf = new byte[tagLen]; - - ret = OpenSSL.EVP_CipherUpdate(_encryptCtx, ciphertext, out tmpLen, - plaintext, (int) plen); - if (ret != 1) throw new CryptoErrorException("openssl: fail to encrypt AEAD"); - clen += (uint) tmpLen; - // For AEAD cipher, it should not output anything - ret = OpenSSL.EVP_CipherFinal_ex(_encryptCtx, ciphertext, ref tmpLen); - if (ret != 1) throw new CryptoErrorException("openssl: fail to finalize AEAD"); - if (tmpLen > 0) - { - throw new System.Exception("openssl: fail to finish AEAD"); - } - - OpenSSL.AEADGetTag(_encryptCtx, tagBuf, tagLen); - Array.Copy(tagBuf, 0, ciphertext, clen, tagLen); - clen += (uint) tagLen; - } - - public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) - { - OpenSSL.SetCtxNonce(_decryptCtx, _decNonce, false); - // buf: ciphertext + tag - // outbuf: plaintext - int ret; - int tmpLen = 0; - plen = 0; - - // split tag - byte[] tagbuf = new byte[tagLen]; - Array.Copy(ciphertext, (int) (clen - tagLen), tagbuf, 0, tagLen); - OpenSSL.AEADSetTag(_decryptCtx, tagbuf, tagLen); - - ret = OpenSSL.EVP_CipherUpdate(_decryptCtx, - plaintext, out tmpLen, ciphertext, (int) (clen - tagLen)); - if (ret != 1) throw new CryptoErrorException("openssl: fail to decrypt AEAD"); - plen += (uint) tmpLen; - - // For AEAD cipher, it should not output anything - ret = OpenSSL.EVP_CipherFinal_ex(_decryptCtx, plaintext, ref tmpLen); - if (ret <= 0) - { - // If this is not successful authenticated - throw new CryptoErrorException(String.Format("ret is {0}", ret)); - } - - if (tmpLen > 0) - { - throw new System.Exception("openssl: fail to finish AEAD"); - } - } - - #region IDisposable - - private bool _disposed; - - // instance based lock - private readonly object _lock = new object(); - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~AEADOpenSSLEncryptor() - { - Dispose(false); - } - - protected virtual void Dispose(bool disposing) - { - lock (_lock) - { - if (_disposed) return; - _disposed = true; - } - - if (disposing) - { - // free managed objects - } - - // free unmanaged objects - if (_encryptCtx != IntPtr.Zero) - { - OpenSSL.EVP_CIPHER_CTX_free(_encryptCtx); - _encryptCtx = IntPtr.Zero; - } - - if (_decryptCtx != IntPtr.Zero) - { - OpenSSL.EVP_CIPHER_CTX_free(_decryptCtx); - _decryptCtx = IntPtr.Zero; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs b/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs deleted file mode 100644 index 06aef5d7a..000000000 --- a/shadowsocks-csharp/Encryption/AEAD/AEADSodiumEncryptor.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Encryption.Exception; - -namespace Shadowsocks.Encryption.AEAD -{ - public class AEADSodiumEncryptor - : AEADEncryptor, IDisposable - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private const int CIPHER_CHACHA20IETFPOLY1305 = 1; - private const int CIPHER_XCHACHA20IETFPOLY1305 = 2; - private const int CIPHER_AES256GCM = 3; - - private byte[] _sodiumEncSubkey; - private byte[] _sodiumDecSubkey; - - public AEADSodiumEncryptor(string method, string password) - : base(method, password) - { - _sodiumEncSubkey = new byte[keyLen]; - _sodiumDecSubkey = new byte[keyLen]; - } - - private static readonly Dictionary _ciphers = new Dictionary - { - {"chacha20-ietf-poly1305", new EncryptorInfo(32, 32, 12, 16, CIPHER_CHACHA20IETFPOLY1305)}, - {"xchacha20-ietf-poly1305", new EncryptorInfo(32, 32, 24, 16, CIPHER_XCHACHA20IETFPOLY1305)}, - {"aes-256-gcm", new EncryptorInfo(32, 32, 12, 16, CIPHER_AES256GCM)}, - }; - - public static List SupportedCiphers() - { - return new List(_ciphers.Keys); - } - - protected override Dictionary getCiphers() - { - return _ciphers; - } - - public override void InitCipher(byte[] salt, bool isEncrypt, bool isUdp) - { - base.InitCipher(salt, isEncrypt, isUdp); - DeriveSessionKey(isEncrypt ? _encryptSalt : _decryptSalt, _Masterkey, - isEncrypt ? _sodiumEncSubkey : _sodiumDecSubkey); - } - - - public override void cipherEncrypt(byte[] plaintext, uint plen, byte[] ciphertext, ref uint clen) - { - Debug.Assert(_sodiumEncSubkey != null); - // buf: all plaintext - // outbuf: ciphertext + tag - int ret; - ulong encClen = 0; - logger.Dump("_encNonce before enc", _encNonce, nonceLen); - logger.Dump("_sodiumEncSubkey", _sodiumEncSubkey, keyLen); - logger.Dump("before cipherEncrypt: plain", plaintext, (int) plen); - switch (_cipher) - { - case CIPHER_CHACHA20IETFPOLY1305: - ret = Sodium.crypto_aead_chacha20poly1305_ietf_encrypt(ciphertext, ref encClen, - plaintext, (ulong) plen, - null, 0, - null, _encNonce, - _sodiumEncSubkey); - break; - case CIPHER_XCHACHA20IETFPOLY1305: - ret = Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(ciphertext, ref encClen, - plaintext, (ulong)plen, - null, 0, - null, _encNonce, - _sodiumEncSubkey); - break; - case CIPHER_AES256GCM: - ret = Sodium.crypto_aead_aes256gcm_encrypt(ciphertext, ref encClen, - plaintext, (ulong)plen, - null, 0, - null, _encNonce, - _sodiumEncSubkey); - break; - default: - throw new System.Exception("not implemented"); - } - if (ret != 0) throw new CryptoErrorException(String.Format("ret is {0}", ret)); - logger.Dump("after cipherEncrypt: cipher", ciphertext, (int) encClen); - clen = (uint) encClen; - } - - public override void cipherDecrypt(byte[] ciphertext, uint clen, byte[] plaintext, ref uint plen) - { - Debug.Assert(_sodiumDecSubkey != null); - // buf: ciphertext + tag - // outbuf: plaintext - int ret; - ulong decPlen = 0; - logger.Dump("_decNonce before dec", _decNonce, nonceLen); - logger.Dump("_sodiumDecSubkey", _sodiumDecSubkey, keyLen); - logger.Dump("before cipherDecrypt: cipher", ciphertext, (int) clen); - switch (_cipher) - { - case CIPHER_CHACHA20IETFPOLY1305: - ret = Sodium.crypto_aead_chacha20poly1305_ietf_decrypt(plaintext, ref decPlen, - null, - ciphertext, (ulong) clen, - null, 0, - _decNonce, _sodiumDecSubkey); - break; - case CIPHER_XCHACHA20IETFPOLY1305: - ret = Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, ref decPlen, - null, - ciphertext, (ulong)clen, - null, 0, - _decNonce, _sodiumDecSubkey); - break; - case CIPHER_AES256GCM: - ret = Sodium.crypto_aead_aes256gcm_decrypt(plaintext, ref decPlen, - null, - ciphertext, (ulong)clen, - null, 0, - _decNonce, _sodiumDecSubkey); - break; - default: - throw new System.Exception("not implemented"); - } - - if (ret != 0) throw new CryptoErrorException(String.Format("ret is {0}", ret)); - logger.Dump("after cipherDecrypt: plain", plaintext, (int) decPlen); - plen = (uint) decPlen; - } - - public override void Dispose() - { - } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/EncryptorFactory.cs b/shadowsocks-csharp/Encryption/EncryptorFactory.cs deleted file mode 100644 index 81df1fe97..000000000 --- a/shadowsocks-csharp/Encryption/EncryptorFactory.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using Shadowsocks.Encryption.AEAD; -using Shadowsocks.Encryption.Stream; - -namespace Shadowsocks.Encryption -{ - public static class EncryptorFactory - { - private static Dictionary _registeredEncryptors = new Dictionary(); - - private static readonly Type[] ConstructorTypes = {typeof(string), typeof(string)}; - - static EncryptorFactory() - { - var AEADMbedTLSEncryptorSupportedCiphers = AEADMbedTLSEncryptor.SupportedCiphers(); - var AEADSodiumEncryptorSupportedCiphers = AEADSodiumEncryptor.SupportedCiphers(); - var PlainEncryptorSupportedCiphers = PlainEncryptor.SupportedCiphers(); - - if (Sodium.AES256GCMAvailable) - { - // prefer to aes-256-gcm in libsodium - AEADMbedTLSEncryptorSupportedCiphers.Remove("aes-256-gcm"); - } - else - { - AEADSodiumEncryptorSupportedCiphers.Remove("aes-256-gcm"); - } - - foreach (string method in AEADOpenSSLEncryptor.SupportedCiphers()) - { - if (!_registeredEncryptors.ContainsKey(method)) - _registeredEncryptors.Add(method, typeof(AEADOpenSSLEncryptor)); - } - - foreach (string method in AEADSodiumEncryptorSupportedCiphers) - { - if (!_registeredEncryptors.ContainsKey(method)) - _registeredEncryptors.Add(method, typeof(AEADSodiumEncryptor)); - } - - foreach (string method in AEADMbedTLSEncryptorSupportedCiphers) - { - if (!_registeredEncryptors.ContainsKey(method)) - _registeredEncryptors.Add(method, typeof(AEADMbedTLSEncryptor)); - } - - foreach (string method in PlainEncryptorSupportedCiphers) - { - if (!_registeredEncryptors.ContainsKey(method)) - _registeredEncryptors.Add(method, typeof(PlainEncryptor)); - } - } - - public static IEncryptor GetEncryptor(string method, string password) - { - if (string.IsNullOrEmpty(method)) - { - method = Model.Server.DefaultMethod; - } - - method = method.ToLowerInvariant(); - Type t = _registeredEncryptors[method]; - - ConstructorInfo c = t.GetConstructor(ConstructorTypes); - if (c == null) throw new System.Exception("Invalid ctor"); - IEncryptor result = (IEncryptor) c.Invoke(new object[] {method, password}); - return result; - } - - public static string DumpRegisteredEncryptor() - { - var sb = new StringBuilder(); - sb.Append(Environment.NewLine); - sb.AppendLine("========================="); - sb.AppendLine("Registered Encryptor Info"); - foreach (var encryptor in _registeredEncryptors) - { - sb.AppendLine(String.Format("{0}=>{1}", encryptor.Key, encryptor.Value.Name)); - } - - sb.AppendLine("========================="); - return sb.ToString(); - } - } -} diff --git a/shadowsocks-csharp/Encryption/MbedTLS.cs b/shadowsocks-csharp/Encryption/MbedTLS.cs deleted file mode 100644 index be6816702..000000000 --- a/shadowsocks-csharp/Encryption/MbedTLS.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Properties; -using Shadowsocks.Util; - -namespace Shadowsocks.Encryption -{ - public static class MbedTLS - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private const string DLLNAME = "libsscrypto.dll"; - - public const int MBEDTLS_ENCRYPT = 1; - public const int MBEDTLS_DECRYPT = 0; - - static MbedTLS() - { - string dllPath = Utils.GetTempPath(DLLNAME); - try - { - FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); - } - catch (IOException) - { - } - catch (System.Exception e) - { - logger.LogUsefulException(e); - } - LoadLibrary(dllPath); - } - - public static byte[] MD5(byte[] input) - { - byte[] output = new byte[16]; - if (md5_ret(input, (uint) input.Length, output) != 0) - throw new System.Exception("mbedtls: MD5 failure"); - return output; - } - - [DllImport("Kernel32.dll")] - private static extern IntPtr LoadLibrary(string path); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int md5_ret(byte[] input, uint ilen, byte[] output); - - /// - /// Get cipher ctx size for unmanaged memory allocation - /// - /// - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_get_size_ex(); - - #region Cipher layer wrappers - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr cipher_info_from_string(string cipher_name); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void cipher_init(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_setup(IntPtr ctx, IntPtr cipher_info); - - // XXX: Check operation before using it - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_setkey(IntPtr ctx, byte[] key, int key_bitlen, int operation); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_set_iv(IntPtr ctx, byte[] iv, int iv_len); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_reset(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_update(IntPtr ctx, byte[] input, int ilen, byte[] output, ref int olen); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void cipher_free(IntPtr ctx); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_auth_encrypt(IntPtr ctx, - byte[] iv, uint iv_len, - IntPtr ad, uint ad_len, - byte[] input, uint ilen, - byte[] output, ref uint olen, - byte[] tag, uint tag_len); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int cipher_auth_decrypt(IntPtr ctx, - byte[] iv, uint iv_len, - IntPtr ad, uint ad_len, - byte[] input, uint ilen, - byte[] output, ref uint olen, - byte[] tag, uint tag_len); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int hkdf(byte[] salt, - int salt_len, byte[] ikm, int ikm_len, - byte[] info, int info_len, byte[] okm, - int okm_len); - - #endregion - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/OpenSSL.cs b/shadowsocks-csharp/Encryption/OpenSSL.cs deleted file mode 100644 index 00056a04c..000000000 --- a/shadowsocks-csharp/Encryption/OpenSSL.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Encryption.Exception; -using Shadowsocks.Properties; -using Shadowsocks.Util; - -namespace Shadowsocks.Encryption -{ - // XXX: only for OpenSSL 1.1.0 and higher - public static class OpenSSL - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private const string DLLNAME = "libsscrypto.dll"; - - public const int OPENSSL_ENCRYPT = 1; - public const int OPENSSL_DECRYPT = 0; - - public const int EVP_CTRL_AEAD_SET_IVLEN = 0x9; - public const int EVP_CTRL_AEAD_GET_TAG = 0x10; - public const int EVP_CTRL_AEAD_SET_TAG = 0x11; - - static OpenSSL() - { - string dllPath = Utils.GetTempPath(DLLNAME); - try - { - FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); - } - catch (IOException) - { - } - catch (System.Exception e) - { - logger.LogUsefulException(e); - } - LoadLibrary(dllPath); - } - - public static IntPtr GetCipherInfo(string cipherName) - { - var name = Encoding.ASCII.GetBytes(cipherName); - Array.Resize(ref name, name.Length + 1); - return EVP_get_cipherbyname(name); - } - - /// - /// Need init cipher context after EVP_CipherFinal_ex to reuse context - /// - /// - /// - /// - public static void SetCtxNonce(IntPtr ctx, byte[] nonce, bool isEncrypt) - { - var ret = EVP_CipherInit_ex(ctx, IntPtr.Zero, - IntPtr.Zero, null, - nonce, - isEncrypt ? OPENSSL_ENCRYPT : OPENSSL_DECRYPT); - if (ret != 1) throw new System.Exception("openssl: fail to set AEAD nonce"); - } - - public static void AEADGetTag(IntPtr ctx, byte[] tagbuf, int taglen) - { - IntPtr tagBufIntPtr = IntPtr.Zero; - try - { - tagBufIntPtr = Marshal.AllocHGlobal(taglen); - var ret = EVP_CIPHER_CTX_ctrl(ctx, - EVP_CTRL_AEAD_GET_TAG, taglen, tagBufIntPtr); - if (ret != 1) throw new CryptoErrorException("openssl: fail to get AEAD tag"); - // take tag from unmanaged memory - Marshal.Copy(tagBufIntPtr, tagbuf, 0, taglen); - } - finally - { - if (tagBufIntPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(tagBufIntPtr); - } - } - } - - public static void AEADSetTag(IntPtr ctx, byte[] tagbuf, int taglen) - { - IntPtr tagBufIntPtr = IntPtr.Zero; - try - { - // allocate unmanaged memory for tag - tagBufIntPtr = Marshal.AllocHGlobal(taglen); - - // copy tag to unmanaged memory - Marshal.Copy(tagbuf, 0, tagBufIntPtr, taglen); - - var ret = EVP_CIPHER_CTX_ctrl(ctx, - EVP_CTRL_AEAD_SET_TAG, taglen, tagBufIntPtr); - - if (ret != 1) throw new CryptoErrorException("openssl: fail to set AEAD tag"); - - } - finally - { - if (tagBufIntPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(tagBufIntPtr); - } - } - } - - [DllImport("Kernel32.dll")] - private static extern IntPtr LoadLibrary(string path); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr EVP_CIPHER_CTX_new(); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern void EVP_CIPHER_CTX_free(IntPtr ctx); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CIPHER_CTX_reset(IntPtr ctx); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CipherInit_ex(IntPtr ctx, IntPtr type, - IntPtr impl, byte[] key, byte[] iv, int enc); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CipherUpdate(IntPtr ctx, byte[] outb, - out int outl, byte[] inb, int inl); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CipherFinal_ex(IntPtr ctx, byte[] outm, ref int outl); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CIPHER_CTX_set_padding(IntPtr x, int padding); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CIPHER_CTX_set_key_length(IntPtr x, int keylen); - - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int EVP_CIPHER_CTX_ctrl(IntPtr ctx, int type, int arg, IntPtr ptr); - - /// - /// simulate NUL-terminated string - /// - /// - /// - [SuppressUnmanagedCodeSecurity] - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr EVP_get_cipherbyname(byte[] name); - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Encryption/Sodium.cs b/shadowsocks-csharp/Encryption/Sodium.cs deleted file mode 100755 index 6b9441dd5..000000000 --- a/shadowsocks-csharp/Encryption/Sodium.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Properties; -using Shadowsocks.Util; - -namespace Shadowsocks.Encryption -{ - public static class Sodium - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private const string DLLNAME = "libsscrypto.dll"; - - private static bool _initialized = false; - private static readonly object _initLock = new object(); - - public static bool AES256GCMAvailable { get; private set; } = false; - - static Sodium() - { - string dllPath = Utils.GetTempPath(DLLNAME); - try - { - FileManager.UncompressFile(dllPath, Resources.libsscrypto_dll); - } - catch (IOException) - { - } - catch (System.Exception e) - { - logger.LogUsefulException(e); - } - LoadLibrary(dllPath); - - lock (_initLock) - { - if (!_initialized) - { - if (sodium_init() == -1) - { - throw new System.Exception("Failed to initialize sodium"); - } - else /* 1 means already initialized; 0 means success */ - { - _initialized = true; - } - - AES256GCMAvailable = crypto_aead_aes256gcm_is_available() == 1; - logger.Debug($"sodium: AES256GCMAvailable is {AES256GCMAvailable}"); - } - } - } - - [DllImport("Kernel32.dll")] - private static extern IntPtr LoadLibrary(string path); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - private static extern int sodium_init(); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - private static extern int crypto_aead_aes256gcm_is_available(); - - #region AEAD - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int sodium_increment(byte[] n, int nlen); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_chacha20poly1305_ietf_encrypt(byte[] c, ref ulong clen_p, byte[] m, - ulong mlen, byte[] ad, ulong adlen, byte[] nsec, byte[] npub, byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_chacha20poly1305_ietf_decrypt(byte[] m, ref ulong mlen_p, - byte[] nsec, byte[] c, ulong clen, byte[] ad, ulong adlen, byte[] npub, byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_xchacha20poly1305_ietf_encrypt(byte[] c, ref ulong clen_p, byte[] m, ulong mlen, - byte[] ad, ulong adlen, byte[] nsec, byte[] npub, byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_xchacha20poly1305_ietf_decrypt(byte[] m, ref ulong mlen_p, byte[] nsec, byte[] c, - ulong clen, byte[] ad, ulong adlen, byte[] npub, byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_aes256gcm_encrypt(byte[] c, ref ulong clen_p, byte[] m, ulong mlen, - byte[] ad, ulong adlen, byte[] nsec, byte[] npub, byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_aead_aes256gcm_decrypt(byte[] m, ref ulong mlen_p, byte[] nsec, byte[] c, - ulong clen, byte[] ad, ulong adlen, byte[] npub, byte[] k); - - #endregion - - #region Stream - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_salsa20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, - byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_chacha20_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, ulong ic, - byte[] k); - - [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern int crypto_stream_chacha20_ietf_xor_ic(byte[] c, byte[] m, ulong mlen, byte[] n, uint ic, - byte[] k); - - #endregion - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/FodyWeavers.xml b/shadowsocks-csharp/FodyWeavers.xml deleted file mode 100644 index 39c0beecd..000000000 --- a/shadowsocks-csharp/FodyWeavers.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/shadowsocks-csharp/FodyWeavers.xsd b/shadowsocks-csharp/FodyWeavers.xsd deleted file mode 100644 index 91102829c..000000000 --- a/shadowsocks-csharp/FodyWeavers.xsd +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of unmanaged 32 bit assembly names to include, delimited with line breaks. - - - - - A list of unmanaged 64 bit assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of unmanaged 32 bit assembly names to include, delimited with |. - - - - - A list of unmanaged 64 bit assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/LocalizationProvider.cs b/shadowsocks-csharp/Localization/LocalizationProvider.cs deleted file mode 100644 index 0677bce19..000000000 --- a/shadowsocks-csharp/Localization/LocalizationProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Reflection; -using WPFLocalizeExtension.Extensions; - -namespace Shadowsocks.Localization -{ - public static class LocalizationProvider - { - public static T GetLocalizedValue(string key) - { - return LocExtension.GetLocalizedValue(Assembly.GetCallingAssembly().GetName().Name + ":Strings:" + key); - } - } -} diff --git a/shadowsocks-csharp/Localization/Strings.Designer.cs b/shadowsocks-csharp/Localization/Strings.Designer.cs deleted file mode 100644 index 6aa6738ac..000000000 --- a/shadowsocks-csharp/Localization/Strings.Designer.cs +++ /dev/null @@ -1,432 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Shadowsocks.Localization { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Shadowsocks.Localization.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to _Add. - /// - internal static string addButton_Content { - get { - return ResourceManager.GetString("addButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Address. - /// - internal static string Address { - get { - return ResourceManager.GetString("Address", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow clients from LAN. - /// - internal static string AllowClientsFromLAN { - get { - return ResourceManager.GetString("AllowClientsFromLAN", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Cancel. - /// - internal static string cancelButton_Content { - get { - return ResourceManager.GetString("cancelButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Copy. - /// - internal static string Copy { - get { - return ResourceManager.GetString("Copy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Copy link. - /// - internal static string copyLinkButton_Content { - get { - return ResourceManager.GetString("copyLinkButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Credentials (optional). - /// - internal static string CredentialsOptional { - get { - return ResourceManager.GetString("CredentialsOptional", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Details. - /// - internal static string Details { - get { - return ResourceManager.GetString("Details", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Forward Proxy. - /// - internal static string ForwardProxy { - get { - return ResourceManager.GetString("ForwardProxy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hotkeys. - /// - internal static string Hotkeys { - get { - return ResourceManager.GetString("Hotkeys", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to HTTP. - /// - internal static string HTTP { - get { - return ResourceManager.GetString("HTTP", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No proxy. - /// - internal static string NoProxy { - get { - return ResourceManager.GetString("NoProxy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Not now. - /// - internal static string notNowButton_Content { - get { - return ResourceManager.GetString("notNowButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _OK. - /// - internal static string okButton_Content { - get { - return ResourceManager.GetString("okButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Online Configuration Delivery. - /// - internal static string OnlineConfigDelivery { - get { - return ResourceManager.GetString("OnlineConfigDelivery", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open logs window. - /// - internal static string OpenLogsWindow { - get { - return ResourceManager.GetString("OpenLogsWindow", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password. - /// - internal static string Password { - get { - return ResourceManager.GetString("Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Port. - /// - internal static string Port { - get { - return ResourceManager.GetString("Port", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Register all. - /// - internal static string registerAllButton_Content { - get { - return ResourceManager.GetString("registerAllButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Register hotkeys at startup. - /// - internal static string RegisterHotkeysAtStartup { - get { - return ResourceManager.GetString("RegisterHotkeysAtStartup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remove. - /// - internal static string removeButton_Content { - get { - return ResourceManager.GetString("removeButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Save. - /// - internal static string saveButton_Content { - get { - return ResourceManager.GetString("saveButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Server Sharing. - /// - internal static string ServerSharing { - get { - return ResourceManager.GetString("ServerSharing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The following sources failed to update:\n\n. - /// - internal static string sip008UpdateAllFailure { - get { - return ResourceManager.GetString("sip008UpdateAllFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Successfully updated all sources!. - /// - internal static string sip008UpdateAllSuccess { - get { - return ResourceManager.GetString("sip008UpdateAllSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Update failed. See the logs for more information.. - /// - internal static string sip008UpdateFailure { - get { - return ResourceManager.GetString("sip008UpdateFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Successfully updated the selected source!. - /// - internal static string sip008UpdateSuccess { - get { - return ResourceManager.GetString("sip008UpdateSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Skip version. - /// - internal static string skipVersionButton_Content { - get { - return ResourceManager.GetString("skipVersionButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SOCKS5. - /// - internal static string SOCKS5 { - get { - return ResourceManager.GetString("SOCKS5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch to next server. - /// - internal static string SwitchToNextServer { - get { - return ResourceManager.GetString("SwitchToNextServer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch to previous server. - /// - internal static string SwitchToPreviousServer { - get { - return ResourceManager.GetString("SwitchToPreviousServer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Timeout (sec). - /// - internal static string Timeout { - get { - return ResourceManager.GetString("Timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle proxy mode. - /// - internal static string ToggleProxyMode { - get { - return ResourceManager.GetString("ToggleProxyMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle system proxy. - /// - internal static string ToggleSystemProxy { - get { - return ResourceManager.GetString("ToggleSystemProxy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type. - /// - internal static string Type { - get { - return ResourceManager.GetString("Type", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Update all. - /// - internal static string updateAllButton_Content { - get { - return ResourceManager.GetString("updateAllButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to _Update. - /// - internal static string updateButton_Content { - get { - return ResourceManager.GetString("updateButton_Content", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please read the release notes carefully. Then decide whether to update.. - /// - internal static string updatePromptBody { - get { - return ResourceManager.GetString("updatePromptBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An update is available.. - /// - internal static string updatePromptTitle { - get { - return ResourceManager.GetString("updatePromptTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username. - /// - internal static string Username { - get { - return ResourceManager.GetString("Username", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to VersionUpdate. - /// - internal static string VersionUpdate { - get { - return ResourceManager.GetString("VersionUpdate", resourceCulture); - } - } - } -} diff --git a/shadowsocks-csharp/Localization/Strings.fr.resx b/shadowsocks-csharp/Localization/Strings.fr.resx deleted file mode 100644 index da708dd24..000000000 --- a/shadowsocks-csharp/Localization/Strings.fr.resx +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Mot de passe - - - Port - - - Type - - - adresse - - - Délai d'attente (sec) - - - Annuler - - - OK - - - Changer l'état de proxy système - - - Changer le mode de proxy système - - - Autoriser les clients du LAN - - - Afficher les journaux - - - Passer au serveur précédent - - - Passer au serveur suivant - - - Enregistrer tout - - - Enregistrer tout au démarrage - - - Ajouter - - - sauter la version - - - Copier le lien - - - retirer - - - mise à jour - - - copie - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.ja.resx b/shadowsocks-csharp/Localization/Strings.ja.resx deleted file mode 100644 index b47f5ba1a..000000000 --- a/shadowsocks-csharp/Localization/Strings.ja.resx +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - パスワード - - - タイプ - - - アドレス - - - ポート - - - タイムアウト (秒) - - - キャンセル - - - OK - - - システム プロキシの状態を切り替える - - - プロキシモードを切り替える - - - LAN からのアクセスの許可を切り替える - - - ログの表示 - - - 前のサーバーに切り替える - - - 次のサーバーに切り替える - - - 全部登録する - - - 起動時にホットキーを登録する - - - 新規 - - - バージョンをスキップ - - - リンクをコピーする - - - 更新 - - - コピー - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.ko.resx b/shadowsocks-csharp/Localization/Strings.ko.resx deleted file mode 100644 index 65c899e49..000000000 --- a/shadowsocks-csharp/Localization/Strings.ko.resx +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 비밀번호 - - - 유형 - - - 주소 - - - 포트 - - - 시간 초과 (초) - - - 취소 - - - 확인 - - - 시스템 프록시 전환 - - - 시스템 프록시 모드 전환 - - - LAN으로부터 클라이언트 허용 - - - 로그 보기… - - - 이전 서버로 전환 - - - 다음 서버로 전환 - - - 모두 등록 - - - 시스템 시작 시 단축키 등록 - - - 추가 - - - 버전 건너 뛰기 - - - 링크 복사 - - - 새롭게 함 - - - - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.resx b/shadowsocks-csharp/Localization/Strings.resx deleted file mode 100644 index 9348c4882..000000000 --- a/shadowsocks-csharp/Localization/Strings.resx +++ /dev/null @@ -1,243 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Type - - - No proxy - - - SOCKS5 - - - HTTP - - - Details - - - Address - - - Port - - - Timeout (sec) - - - Credentials (optional) - - - Username - - - Password - - - _Save - - - _Cancel - - - _OK - - - Toggle system proxy - - - Toggle proxy mode - - - Allow clients from LAN - - - Open logs window - - - Switch to previous server - - - Switch to next server - - - Register hotkeys at startup - - - _Register all - - - _Update - - - Update all - - - _Copy link - - - Remove - - - _Add - - - An update is available. - - - Please read the release notes carefully. Then decide whether to update. - - - _Skip version - - - _Not now - - - _Copy - - - Forward Proxy - - - Server Sharing - - - Hotkeys - - - Online Configuration Delivery - - - VersionUpdate - - - Successfully updated the selected source! - - - Update failed. See the logs for more information. - - - Successfully updated all sources! - - - The following sources failed to update:\n\n - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.ru.resx b/shadowsocks-csharp/Localization/Strings.ru.resx deleted file mode 100644 index ce0074480..000000000 --- a/shadowsocks-csharp/Localization/Strings.ru.resx +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Пароль - - - тип - - - адрес - - - порт - - - Таймаут (сек) - - - Отмена - - - ОК - - - ВКЛ/ВЫКЛ системный прокси-сервер - - - Переключение режима прокси-сервера - - - Общий доступ к подключению - - - Просмотр журналов - - - Переключить на пред. сервер - - - Переключить на след. сервер - - - Применить все - - - Применять при запуске программы - - - Добавить - - - пропустить версию - - - копировать ссылку - - - Удалить - - - Обновить - - - копировать - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.zh-Hans.resx b/shadowsocks-csharp/Localization/Strings.zh-Hans.resx deleted file mode 100644 index f3ef95200..000000000 --- a/shadowsocks-csharp/Localization/Strings.zh-Hans.resx +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 不使用代理 - - - 类型 - - - 认证 (可选) - - - 详细设置 - - - 地址 - - - 保存 - - - 取消 - - - 用户名 - - - 密码 - - - 端口 - - - 超时 (秒) - - - 确定 - - - 全部注册 - - - 切换系统代理 - - - 切换代理模式 - - - 允许内网客户端连接 - - - 打开日志窗口 - - - 切换到上一个服务器 - - - 切换到下一个服务器 - - - 启动时注册快捷键 - - - 更新 - - - 全部更新 - - - 复制链接 - - - 移除 - - - 添加 - - - 跳过版本 - - - 暂不更新 - - - 发现可用更新 - - - 请仔细阅读发布信息,然后决定是否更新。 - - - 复制 - - - 前置代理 - - - 在线配置下发 - - - 热键 - - - 服务器分享 - - - 版本更新 - - - 成功更新选定来源! - - - 更新失败,请查看日志获取更多信息。 - - - 成功更新所有来源! - - - 下列来源更新失败:\n\n - - \ No newline at end of file diff --git a/shadowsocks-csharp/Localization/Strings.zh-Hant.resx b/shadowsocks-csharp/Localization/Strings.zh-Hant.resx deleted file mode 100644 index e52f7e3f0..000000000 --- a/shadowsocks-csharp/Localization/Strings.zh-Hant.resx +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 密碼 - - - Port - - - 類型 - - - 位址 - - - 逾時 (秒) - - - 取消 - - - 確定 - - - 切換系統 Proxy 狀態 - - - 切換系統 Proxy 模式 - - - 切換區域網路共用 - - - 顯示記錄檔 - - - 切換上一個伺服器 - - - 切換下一個伺服器 - - - 註冊所有快速鍵 - - - 啟動時註冊快速鍵 - - - 更新 - - - 全部更新 - - - 新增 - - - 跳過版本 - - - 暫不更新 - - - 發現可用更新 - - - 複製鏈接 - - - 複製 - - \ No newline at end of file diff --git a/shadowsocks-csharp/Model/Configuration.cs b/shadowsocks-csharp/Model/Configuration.cs deleted file mode 100644 index 5cf6b5348..000000000 --- a/shadowsocks-csharp/Model/Configuration.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Windows; -using Newtonsoft.Json; -using NLog; -using Shadowsocks.Controller; - -namespace Shadowsocks.Model -{ - [Serializable] - public class Configuration - { - [JsonIgnore] - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - - public string version; - - public List configs; - - public List onlineConfigSource; - - // when strategy is set, index is ignored - public string strategy; - public int index; - public bool global; - public bool enabled; - public bool shareOverLan; - public bool firstRun; - public int localPort; - public bool portableMode; - public bool showPluginOutput; - public string pacUrl; - - public bool useOnlinePac; - public bool secureLocalPac; // enable secret for PAC server - public bool regeneratePacOnUpdate; // regenerate pac.txt on version update - public bool autoCheckUpdate; - public bool checkPreRelease; - public string skippedUpdateVersion; // skip the update with this version number - public bool isVerboseLogging; - - // hidden options - public bool isIPv6Enabled; // for experimental ipv6 support - public bool generateLegacyUrl; // for pre-sip002 url compatibility - public string geositeUrl; // for custom geosite source (and rule group) - public string geositeSha256sumUrl; // optional custom sha256sum url, leave empty to disable checksum verification for your custom geosite source - public List geositeDirectGroups; // groups of domains that we connect without the proxy - public List geositeProxiedGroups; // groups of domains that we connect via the proxy - public bool geositePreferDirect; // a.k.a blacklist mode - public string userAgent; - - //public NLogConfig.LogLevel logLevel; - public LogViewerConfig logViewer; - public ForwardProxyConfig proxy; - public HotkeyConfig hotkey; - - [JsonIgnore] - public bool firstRunOnNewVersion; - - public Configuration() - { - version = UpdateChecker.Version; - strategy = ""; - index = 0; - global = false; - enabled = false; - shareOverLan = false; - firstRun = true; - localPort = 1080; - portableMode = true; - showPluginOutput = false; - pacUrl = ""; - useOnlinePac = false; - secureLocalPac = true; - regeneratePacOnUpdate = true; - autoCheckUpdate = false; - checkPreRelease = false; - skippedUpdateVersion = ""; - isVerboseLogging = false; - - // hidden options - isIPv6Enabled = false; - generateLegacyUrl = false; - geositeUrl = ""; - geositeSha256sumUrl = ""; - geositeDirectGroups = new List() - { - "private", - "cn", - "geolocation-!cn@cn", - }; - geositeProxiedGroups = new List() - { - "geolocation-!cn", - }; - geositePreferDirect = false; - userAgent = "ShadowsocksWindows/$version"; - - logViewer = new LogViewerConfig(); - proxy = new ForwardProxyConfig(); - hotkey = new HotkeyConfig(); - - firstRunOnNewVersion = false; - - configs = new List(); - onlineConfigSource = new List(); - } - - [JsonIgnore] - public string userAgentString; // $version substituted with numeral version in it - - [JsonIgnore] - NLogConfig nLogConfig; - - private static readonly string CONFIG_FILE = "gui-config.json"; -#if DEBUG - private static readonly NLogConfig.LogLevel verboseLogLevel = NLogConfig.LogLevel.Trace; -#else - private static readonly NLogConfig.LogLevel verboseLogLevel = NLogConfig.LogLevel.Debug; -#endif - - [JsonIgnore] - public string LocalHost => isIPv6Enabled ? "[::1]" : "127.0.0.1"; - - public Server GetCurrentServer() - { - if (index >= 0 && index < configs.Count) - return configs[index]; - else - return GetDefaultServer(); - } - - public WebProxy WebProxy => enabled - ? new WebProxy( - isIPv6Enabled - ? $"[{IPAddress.IPv6Loopback}]" - : IPAddress.Loopback.ToString(), - localPort) - : null; - - /// - /// Used by multiple forms to validate a server. - /// Communication is done by throwing exceptions. - /// - /// - public static void CheckServer(Server server) - { - CheckServer(server.server); - CheckPort(server.server_port); - CheckPassword(server.password); - CheckTimeout(server.timeout, Server.MaxServerTimeoutSec); - } - - /// - /// Loads the configuration from file. - /// - /// An Configuration object. - public static Configuration Load() - { - Configuration config; - if (File.Exists(CONFIG_FILE)) - { - try - { - string configContent = File.ReadAllText(CONFIG_FILE); - config = JsonConvert.DeserializeObject(configContent, new JsonSerializerSettings() - { - ObjectCreationHandling = ObjectCreationHandling.Replace - }); - return config; - } - catch (Exception e) - { - if (!(e is FileNotFoundException)) - logger.LogUsefulException(e); - } - } - config = new Configuration(); - return config; - } - - /// - /// Process the loaded configurations and set up things. - /// - /// A reference of Configuration object. - public static void Process(ref Configuration config) - { - // Verify if the configured geosite groups exist. - // Reset to default if ANY one of the configured group doesn't exist. - if (!ValidateGeositeGroupList(config.geositeDirectGroups)) - ResetGeositeDirectGroup(ref config.geositeDirectGroups); - if (!ValidateGeositeGroupList(config.geositeProxiedGroups)) - ResetGeositeProxiedGroup(ref config.geositeProxiedGroups); - - // Mark the first run of a new version. - var appVersion = new Version(UpdateChecker.Version); - var configVersion = new Version(config.version); - if (appVersion.CompareTo(configVersion) > 0) - { - config.firstRunOnNewVersion = true; - } - // Add an empty server configuration - if (config.configs.Count == 0) - config.configs.Add(GetDefaultServer()); - // Selected server - if (config.index == -1 && string.IsNullOrEmpty(config.strategy)) - config.index = 0; - if (config.index >= config.configs.Count) - config.index = config.configs.Count - 1; - // Check OS IPv6 support - if (!System.Net.Sockets.Socket.OSSupportsIPv6) - config.isIPv6Enabled = false; - config.proxy.CheckConfig(); - // Replace $version with the version number. - config.userAgentString = config.userAgent.Replace("$version", config.version); - - // NLog log level - try - { - config.nLogConfig = NLogConfig.LoadXML(); - switch (config.nLogConfig.GetLogLevel()) - { - case NLogConfig.LogLevel.Fatal: - case NLogConfig.LogLevel.Error: - case NLogConfig.LogLevel.Warn: - case NLogConfig.LogLevel.Info: - config.isVerboseLogging = false; - break; - case NLogConfig.LogLevel.Debug: - case NLogConfig.LogLevel.Trace: - config.isVerboseLogging = true; - break; - } - } - catch (Exception e) - { - MessageBox.Show($"Cannot get the log level from NLog config file. Please check if the nlog config file exists with corresponding XML nodes.\n{e.Message}"); - } - } - - /// - /// Saves the Configuration object to file. - /// - /// A Configuration object. - public static void Save(Configuration config) - { - config.configs = SortByOnlineConfig(config.configs); - - FileStream configFileStream = null; - StreamWriter configStreamWriter = null; - try - { - configFileStream = File.Open(CONFIG_FILE, FileMode.Create); - configStreamWriter = new StreamWriter(configFileStream); - var jsonString = JsonConvert.SerializeObject(config, Formatting.Indented); - configStreamWriter.Write(jsonString); - configStreamWriter.Flush(); - // NLog - config.nLogConfig.SetLogLevel(config.isVerboseLogging ? verboseLogLevel : NLogConfig.LogLevel.Info); - NLogConfig.SaveXML(config.nLogConfig); - } - catch (Exception e) - { - logger.LogUsefulException(e); - } - finally - { - if (configStreamWriter != null) - configStreamWriter.Dispose(); - if (configFileStream != null) - configFileStream.Dispose(); - } - } - - public static List SortByOnlineConfig(IEnumerable servers) - { - var groups = servers.GroupBy(s => s.group); - List ret = new List(); - ret.AddRange(groups.Where(g => string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); - ret.AddRange(groups.Where(g => !string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); - return ret; - } - - /// - /// Validates if the groups in the list are all valid. - /// - /// The list of groups to validate. - /// - /// True if all groups are valid. - /// False if any one of them is invalid. - /// - public static bool ValidateGeositeGroupList(List groups) - { - foreach (var geositeGroup in groups) - if (!GeositeUpdater.CheckGeositeGroup(geositeGroup)) // found invalid group - { -#if DEBUG - logger.Debug($"Available groups:"); - foreach (var group in GeositeUpdater.Geosites.Keys) - logger.Debug($"{group}"); -#endif - logger.Warn($"The Geosite group {geositeGroup} doesn't exist. Resetting to default groups."); - return false; - } - return true; - } - - public static void ResetGeositeDirectGroup(ref List geositeDirectGroups) - { - geositeDirectGroups.Clear(); - geositeDirectGroups.Add("private"); - geositeDirectGroups.Add("cn"); - geositeDirectGroups.Add("geolocation-!cn@cn"); - } - - public static void ResetGeositeProxiedGroup(ref List geositeProxiedGroups) - { - geositeProxiedGroups.Clear(); - geositeProxiedGroups.Add("geolocation-!cn"); - } - - public static void ResetUserAgent(Configuration config) - { - config.userAgent = "ShadowsocksWindows/$version"; - config.userAgentString = config.userAgent.Replace("$version", config.version); - } - - public static Server AddDefaultServerOrServer(Configuration config, Server server = null, int? index = null) - { - if (config?.configs != null) - { - server = (server ?? GetDefaultServer()); - - config.configs.Insert(index.GetValueOrDefault(config.configs.Count), server); - - //if (index.HasValue) - // config.configs.Insert(index.Value, server); - //else - // config.configs.Add(server); - } - return server; - } - - public static Server GetDefaultServer() - { - return new Server(); - } - - public static void CheckPort(int port) - { - if (port <= 0 || port > 65535) - throw new ArgumentException(I18N.GetString("Port out of range")); - } - - public static void CheckLocalPort(int port) - { - CheckPort(port); - if (port == 8123) - throw new ArgumentException(I18N.GetString("Port can't be 8123")); - } - - private static void CheckPassword(string password) - { - if (string.IsNullOrEmpty(password)) - throw new ArgumentException(I18N.GetString("Password can not be blank")); - } - - public static void CheckServer(string server) - { - if (string.IsNullOrEmpty(server)) - throw new ArgumentException(I18N.GetString("Server IP can not be blank")); - } - - public static void CheckTimeout(int timeout, int maxTimeout) - { - if (timeout <= 0 || timeout > maxTimeout) - throw new ArgumentException( - I18N.GetString("Timeout is invalid, it should not exceed {0}", maxTimeout)); - } - } -} diff --git a/shadowsocks-csharp/Model/LogViewerConfig.cs b/shadowsocks-csharp/Model/LogViewerConfig.cs deleted file mode 100644 index 6a2fc8620..000000000 --- a/shadowsocks-csharp/Model/LogViewerConfig.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Drawing; -using System.Windows.Forms; -using Newtonsoft.Json; - -namespace Shadowsocks.Model -{ - [Serializable] - public class LogViewerConfig - { - public bool topMost; - public bool wrapText; - public bool toolbarShown; - - public Font Font { get; set; } = new Font("Consolas", 8F); - - public Color BackgroundColor { get; set; } = Color.Black; - - public Color TextColor { get; set; } = Color.White; - - public LogViewerConfig() - { - topMost = false; - wrapText = false; - toolbarShown = false; - } - - - #region Size - - public void SaveSize() - { - Properties.Settings.Default.Save(); - } - - [JsonIgnore] - public int Width - { - get { return Properties.Settings.Default.LogViewerWidth; } - set { Properties.Settings.Default.LogViewerWidth = value; } - } - - [JsonIgnore] - public int Height - { - get { return Properties.Settings.Default.LogViewerHeight; } - set { Properties.Settings.Default.LogViewerHeight = value; } - } - [JsonIgnore] - public int Top - { - get { return Properties.Settings.Default.LogViewerTop; } - set { Properties.Settings.Default.LogViewerTop = value; } - } - [JsonIgnore] - public int Left - { - get { return Properties.Settings.Default.LogViewerLeft; } - set { Properties.Settings.Default.LogViewerLeft = value; } - } - [JsonIgnore] - public bool Maximized - { - get { return Properties.Settings.Default.LogViewerMaximized; } - set { Properties.Settings.Default.LogViewerMaximized = value; } - } - - [JsonIgnore] - // Use GetBestTop() and GetBestLeft() to ensure the log viwer form can be always display IN screen. - public int BestLeft - { - get - { - int width = Width; - width = (width >= 400) ? width : 400; // set up the minimum size - return Screen.PrimaryScreen.WorkingArea.Width - width; - } - } - - [JsonIgnore] - public int BestTop - { - get - { - int height = Height; - height = (height >= 200) ? height : 200; // set up the minimum size - return Screen.PrimaryScreen.WorkingArea.Height - height; - } - } - - #endregion - - } -} diff --git a/shadowsocks-csharp/Model/NlogConfig.cs b/shadowsocks-csharp/Model/NlogConfig.cs deleted file mode 100644 index 27c930155..000000000 --- a/shadowsocks-csharp/Model/NlogConfig.cs +++ /dev/null @@ -1,135 +0,0 @@ -using NLog; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; - -namespace Shadowsocks.Model -{ - public class NLogConfig - { - public enum LogLevel - { - Fatal, - Error, - Warn, - Info, - Debug, - Trace, - } - - const string NLOG_CONFIG_FILE_NAME = "NLog.config"; - const string TARGET_MIN_LEVEL_ATTRIBUTE = "minlevel"; - const string LOGGER_FILE_NAME_ATTRIBUTE = "fileName"; - - XmlDocument doc = new XmlDocument(); - XmlElement logFileNameElement; - XmlElement logLevelElement; - - /// - /// Load the NLog config xml file content - /// - public static NLogConfig LoadXML() - { - NLogConfig config = new NLogConfig(); - config.doc.Load(NLOG_CONFIG_FILE_NAME); - config.logLevelElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:logger[@name='*']"); - config.logFileNameElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:target[@name='file']"); - return config; - } - - /// - /// Save the content to NLog config xml file - /// - public static void SaveXML(NLogConfig nLogConfig) - { - nLogConfig.doc.Save(NLOG_CONFIG_FILE_NAME); - } - - - /// - /// Get the current minLogLevel from xml file - /// - /// - public LogLevel GetLogLevel() - { - LogLevel level = LogLevel.Warn; - string levelStr = logLevelElement.GetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE); - Enum.TryParse(levelStr, out level); - return level; - } - - /// - /// Get the target fileName from xml file - /// - /// - public string GetLogFileName() - { - return logFileNameElement.GetAttribute(LOGGER_FILE_NAME_ATTRIBUTE); - } - - /// - /// Set the minLogLevel to xml file - /// - /// - public void SetLogLevel(LogLevel logLevel) - { - logLevelElement.SetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE, logLevel.ToString("G")); - } - - /// - /// Set the target fileName to xml file - /// - /// - public void SetLogFileName(string fileName) - { - logFileNameElement.SetAttribute(LOGGER_FILE_NAME_ATTRIBUTE, fileName); - } - - /// - /// Select a single XML node/elemant - /// - /// - /// - /// - private static XmlNode SelectSingleNode(XmlDocument doc, string xpath) - { - XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable); - manager.AddNamespace("nlog", "http://www.nlog-project.org/schemas/NLog.xsd"); - //return doc.SelectSingleNode("//nlog:logger[(@shadowsocks='managed') and (@name='*')]", manager); - return doc.SelectSingleNode(xpath, manager); - } - - /// - /// Extract the pre-defined NLog configuration file is does not exist. Then reload the Nlog configuration. - /// - public static void TouchAndApplyNLogConfig() - { - try - { - if (File.Exists(NLOG_CONFIG_FILE_NAME)) - return; // NLog.config exists, and has already been loaded - - File.WriteAllText(NLOG_CONFIG_FILE_NAME, Properties.Resources.NLog_config); - } - catch (Exception ex) - { - NLog.Common.InternalLogger.Error(ex, "[shadowsocks] Failed to setup default NLog.config: {0}", NLOG_CONFIG_FILE_NAME); - return; - } - - LoadConfiguration(); // Load the new config-file - } - - /// - /// NLog reload the config file and apply to current LogManager - /// - public static void LoadConfiguration() - { - LogManager.LoadConfiguration(NLOG_CONFIG_FILE_NAME); - } - } -} diff --git a/shadowsocks-csharp/Model/Server.cs b/shadowsocks-csharp/Model/Server.cs deleted file mode 100755 index 7eb71e974..000000000 --- a/shadowsocks-csharp/Model/Server.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; -using System.Web; -using Shadowsocks.Controller; -using System.Text.RegularExpressions; -using System.Linq; -using Newtonsoft.Json; -using System.ComponentModel; - -namespace Shadowsocks.Model -{ - [Serializable] - public class Server - { - public const string DefaultMethod = "chacha20-ietf-poly1305"; - public const int DefaultPort = 8388; - - #region ParseLegacyURL - private static readonly Regex UrlFinder = new Regex(@"ss://(?[A-Za-z0-9+-/=_]+)(?:#(?\S+))?", RegexOptions.IgnoreCase); - private static readonly Regex DetailsParser = new Regex(@"^((?.+?):(?.*)@(?.+?):(?\d+?))$", RegexOptions.IgnoreCase); - #endregion ParseLegacyURL - - private const int DefaultServerTimeoutSec = 5; - public const int MaxServerTimeoutSec = 20; - - public string server; - public int server_port; - public string password; - public string method; - // optional fields - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin_opts; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin_args; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string remarks; - - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string group; - - public int timeout; - - // Set to true when imported from a legacy ss:// URL. - public bool warnLegacyUrl; - - public override int GetHashCode() - { - return server.GetHashCode() ^ server_port; - } - - public override bool Equals(object obj) => obj is Server o2 && server == o2.server && server_port == o2.server_port; - - public override string ToString() - { - if (string.IsNullOrEmpty(server)) - { - return I18N.GetString("New server"); - } - - string serverStr = $"{FormalHostName}:{server_port}"; - return string.IsNullOrEmpty(remarks) - ? serverStr - : $"{remarks} ({serverStr})"; - } - - public string GetURL(bool legacyUrl = false) - { - string tag = string.Empty; - string url = string.Empty; - - if (legacyUrl && string.IsNullOrWhiteSpace(plugin)) - { - // For backwards compatiblity, if no plugin, use old url format - string parts = $"{method}:{password}@{server}:{server_port}"; - string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); - url = base64; - } - else - { - // SIP002 - string parts = $"{method}:{password}"; - string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); - string websafeBase64 = base64.Replace('+', '-').Replace('/', '_').TrimEnd('='); - - url = string.Format( - "{0}@{1}:{2}/", - websafeBase64, - FormalHostName, - server_port - ); - - if (!string.IsNullOrWhiteSpace(plugin)) - { - - string pluginPart = plugin; - if (!string.IsNullOrWhiteSpace(plugin_opts)) - { - pluginPart += ";" + plugin_opts; - } - string pluginQuery = "?plugin=" + HttpUtility.UrlEncode(pluginPart, Encoding.UTF8); - url += pluginQuery; - } - } - - if (!string.IsNullOrEmpty(remarks)) - { - tag = $"#{HttpUtility.UrlEncode(remarks, Encoding.UTF8)}"; - } - return $"ss://{url}{tag}"; - } - - [JsonIgnore] - public string FormalHostName - { - get - { - // CheckHostName() won't do a real DNS lookup - switch (Uri.CheckHostName(server)) - { - case UriHostNameType.IPv6: // Add square bracket when IPv6 (RFC3986) - return $"[{server}]"; - default: // IPv4 or domain name - return server; - } - } - } - - public Server() - { - server = ""; - server_port = DefaultPort; - method = DefaultMethod; - plugin = ""; - plugin_opts = ""; - plugin_args = ""; - password = ""; - remarks = ""; - timeout = DefaultServerTimeoutSec; - } - - private static Server ParseLegacyURL(string ssURL) - { - var match = UrlFinder.Match(ssURL); - if (!match.Success) - return null; - - Server server = new Server(); - var base64 = match.Groups["base64"].Value.TrimEnd('/'); - var tag = match.Groups["tag"].Value; - if (!string.IsNullOrEmpty(tag)) - { - server.remarks = HttpUtility.UrlDecode(tag, Encoding.UTF8); - } - Match details = null; - try - { - details = DetailsParser.Match(Encoding.UTF8.GetString(Convert.FromBase64String( - base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')))); - } - catch (FormatException) - { - return null; - } - if (!details.Success) - return null; - server.method = details.Groups["method"].Value; - server.password = details.Groups["password"].Value; - server.server = details.Groups["hostname"].Value; - server.server_port = int.Parse(details.Groups["port"].Value); - server.warnLegacyUrl = true; - return server; - } - - public static Server ParseURL(string serverUrl) - { - string _serverUrl = serverUrl.Trim(); - if (!_serverUrl.StartsWith("ss://", StringComparison.InvariantCultureIgnoreCase)) - { - return null; - } - - Server legacyServer = ParseLegacyURL(serverUrl); - if (legacyServer != null) //legacy - { - return legacyServer; - } - else //SIP002 - { - Uri parsedUrl; - try - { - parsedUrl = new Uri(serverUrl); - } - catch (UriFormatException) - { - return null; - } - Server server = new Server - { - remarks = HttpUtility.UrlDecode(parsedUrl.GetComponents( - UriComponents.Fragment, UriFormat.Unescaped), Encoding.UTF8), - server = parsedUrl.IdnHost, - server_port = parsedUrl.Port, - }; - - // parse base64 UserInfo - string rawUserInfo = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); - string base64 = rawUserInfo.Replace('-', '+').Replace('_', '/'); // Web-safe base64 to normal base64 - string userInfo = ""; - try - { - userInfo = Encoding.UTF8.GetString(Convert.FromBase64String( - base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='))); - } - catch (FormatException) - { - return null; - } - string[] userInfoParts = userInfo.Split(new char[] { ':' }, 2); - if (userInfoParts.Length != 2) - { - return null; - } - server.method = userInfoParts[0]; - server.password = userInfoParts[1]; - - NameValueCollection queryParameters = HttpUtility.ParseQueryString(parsedUrl.Query); - string[] pluginParts = (queryParameters["plugin"] ?? "").Split(new[] { ';' }, 2); - if (pluginParts.Length > 0) - { - server.plugin = pluginParts[0] ?? ""; - } - - if (pluginParts.Length > 1) - { - server.plugin_opts = pluginParts[1] ?? ""; - } - - return server; - } - } - - public static List GetServers(string ssURL) - { - return ssURL - .Split('\r', '\n', ' ') - .Select(u => ParseURL(u)) - .Where(s => s != null) - .ToList(); - } - - public string Identifier() - { - return server + ':' + server_port; - } - } -} diff --git a/shadowsocks-csharp/Model/SysproxyConfig.cs b/shadowsocks-csharp/Model/SysproxyConfig.cs deleted file mode 100644 index 53e78a6ef..000000000 --- a/shadowsocks-csharp/Model/SysproxyConfig.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Shadowsocks.Model -{ - /* - * Data come from WinINET - */ - - [Serializable] - public class SysproxyConfig - { - public bool UserSettingsRecorded; - public string Flags; - public string ProxyServer; - public string BypassList; - public string PacUrl; - - public SysproxyConfig() - { - UserSettingsRecorded = false; - Flags = "1"; - // Watchout, Nullable! See #2100 - ProxyServer = ""; - BypassList = ""; - PacUrl = ""; - } - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/Program.cs b/shadowsocks-csharp/Program.cs deleted file mode 100755 index 39c5b9786..000000000 --- a/shadowsocks-csharp/Program.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Net; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using CommandLine; -using Microsoft.Win32; -using NLog; -using ReactiveUI; -using Shadowsocks.Controller; -using Shadowsocks.Controller.Hotkeys; -using Shadowsocks.Util; -using Shadowsocks.View; -using Splat; -using WPFLocalizeExtension.Engine; - -namespace Shadowsocks -{ - internal static class Program - { - private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - - public static ShadowsocksController MainController { get; private set; } - public static MenuViewController MenuController { get; private set; } - public static CommandLineOption Options { get; private set; } - public static string[] Args { get; private set; } - - // https://github.com/dotnet/runtime/issues/13051#issuecomment-510267727 - public static readonly string ExecutablePath = Process.GetCurrentProcess().MainModule?.FileName; - public static readonly string WorkingDirectory = Path.GetDirectoryName(ExecutablePath); - - private static readonly Mutex mutex = new Mutex(true, $"Shadowsocks_{ExecutablePath.GetHashCode()}"); - - /// - /// 应用程序的主入口点。 - /// - /// - [STAThread] - private static void Main(string[] args) - { - #region Single Instance and IPC - bool hasAnotherInstance = !mutex.WaitOne(TimeSpan.Zero, true); - - // store args for further use - Args = args; - Parser.Default.ParseArguments(args) - .WithParsed(opt => Options = opt) - .WithNotParsed(e => e.Output()); - - if (hasAnotherInstance) - { - if (!string.IsNullOrWhiteSpace(Options.OpenUrl)) - { - IPCService.RequestOpenUrl(Options.OpenUrl); - } - else - { - MessageBox.Show(I18N.GetString("Find Shadowsocks icon in your notify tray.") - + Environment.NewLine - + I18N.GetString("If you want to start multiple Shadowsocks, make a copy in another directory."), - I18N.GetString("Shadowsocks is already running.")); - } - return; - } - #endregion - - #region Enviroment Setup - Directory.SetCurrentDirectory(WorkingDirectory); - // todo: initialize the NLog configuartion - Model.NLogConfig.TouchAndApplyNLogConfig(); - - // .NET Framework 4.7.2 on Win7 compatibility - ServicePointManager.SecurityProtocol |= - SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; - #endregion - - #region Compactibility Check - // Check OS since we are using dual-mode socket - if (!Utils.IsWinVistaOrHigher()) - { - MessageBox.Show(I18N.GetString("Unsupported operating system, use Windows Vista at least."), - "Shadowsocks Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - - // Check .NET Framework version - if (!Utils.IsSupportedRuntimeVersion()) - { - if (DialogResult.OK == MessageBox.Show(I18N.GetString("Unsupported .NET Framework, please update to {0} or later.", "4.8"), - "Shadowsocks Error", MessageBoxButtons.OKCancel, MessageBoxIcon.Error)) - { - Process.Start("https://dotnet.microsoft.com/download/dotnet-framework/net48"); - } - return; - } - #endregion - - #region Event Handlers Setup - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); - // handle UI exceptions - Application.ThreadException += Application_ThreadException; - // handle non-UI exceptions - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - Application.ApplicationExit += Application_ApplicationExit; - SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - AutoStartup.RegisterForRestart(true); - #endregion - - // We would use this in v5. - // Parameters would have to be dropped from views' constructors (VersionUpdatePromptView) - //Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly()); - - // Workaround for hosting WPF controls in a WinForms app. - // We have to manually set the culture for the LocalizeDictionary instance. - // https://stackoverflow.com/questions/374518/localizing-a-winforms-application-with-embedded-wpf-user-controls - // https://stackoverflow.com/questions/14668640/wpf-localize-extension-translate-window-at-run-time - LocalizeDictionary.Instance.Culture = Thread.CurrentThread.CurrentCulture; - -#if DEBUG - // truncate privoxy log file while debugging - string privoxyLogFilename = Utils.GetTempPath("privoxy.log"); - if (File.Exists(privoxyLogFilename)) - using (new FileStream(privoxyLogFilename, FileMode.Truncate)) { } -#endif - MainController = new ShadowsocksController(); - MenuController = new MenuViewController(MainController); - - HotKeys.Init(MainController); - MainController.Start(); - - // Update online config - Task.Run(async () => - { - await Task.Delay(10 * 1000); - await MainController.UpdateAllOnlineConfig(); - }); - -#region IPC Handler and Arguement Process - IPCService ipcService = new IPCService(); - Task.Run(() => ipcService.RunServer()); - ipcService.OpenUrlRequested += (_1, e) => MainController.AskAddServerBySSURL(e.Url); - - if (!string.IsNullOrWhiteSpace(Options.OpenUrl)) - { - MainController.AskAddServerBySSURL(Options.OpenUrl); - } -#endregion - - Application.Run(); - - } - - private static int exited = 0; - private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - if (Interlocked.Increment(ref exited) == 1) - { - string errMsg = e.ExceptionObject.ToString(); - logger.Error(errMsg); - MessageBox.Show( - $"{I18N.GetString("Unexpected error, shadowsocks will exit. Please report to")} https://github.com/shadowsocks/shadowsocks-windows/issues {Environment.NewLine}{errMsg}", - "Shadowsocks non-UI Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - Application.Exit(); - } - } - - private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) - { - if (Interlocked.Increment(ref exited) == 1) - { - string errorMsg = $"Exception Detail: {Environment.NewLine}{e.Exception}"; - logger.Error(errorMsg); - MessageBox.Show( - $"{I18N.GetString("Unexpected error, shadowsocks will exit. Please report to")} https://github.com/shadowsocks/shadowsocks-windows/issues {Environment.NewLine}{errorMsg}", - "Shadowsocks UI Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - Application.Exit(); - } - } - - private static void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) - { - switch (e.Mode) - { - case PowerModes.Resume: - logger.Info("os wake up"); - if (MainController != null) - { - Task.Factory.StartNew(() => - { - Thread.Sleep(10 * 1000); - try - { - MainController.Start(true); - logger.Info("controller started"); - } - catch (Exception ex) - { - logger.LogUsefulException(ex); - } - }); - } - break; - case PowerModes.Suspend: - if (MainController != null) - { - MainController.Stop(); - logger.Info("controller stopped"); - } - logger.Info("os suspend"); - break; - } - } - - private static void Application_ApplicationExit(object sender, EventArgs e) - { - // detach static event handlers - Application.ApplicationExit -= Application_ApplicationExit; - SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; - Application.ThreadException -= Application_ThreadException; - HotKeys.Destroy(); - if (MainController != null) - { - MainController.Stop(); - MainController = null; - } - } - } -} diff --git a/shadowsocks-csharp/Properties/AssemblyInfo.cs b/shadowsocks-csharp/Properties/AssemblyInfo.cs deleted file mode 100755 index b140accfc..000000000 --- a/shadowsocks-csharp/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Shadowsocks.Controller; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// 有关程序集的常规信息通过下列属性集 -// 控制。更改这些属性值可修改 -// 与程序集关联的信息。 -[assembly: AssemblyTitle("Shadowsocks")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Shadowsocks")] -[assembly: AssemblyCopyright("clowwindy & community 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// 将 ComVisible 设置为 false 使此程序集中的类型 -// 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型, -// 则将该类型上的 ComVisible 属性设置为 true。 -[assembly: ComVisible(false)] - -// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID -[assembly: Guid("f8334709-4309-436a-8bbd-6165dcf4a660")] - -// 程序集的版本信息由下面四个值组成: -// -// 主版本 -// 次版本 -// 内部版本号 -// 修订号 -// -// 可以指定所有这些值,也可以使用“内部版本号”和“修订号”的默认值, -// 方法是按如下所示使用“*”: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion(UpdateChecker.Version)] -// [assembly: AssemblyFileVersion("2.0.0")] diff --git a/shadowsocks-csharp/Properties/Resources.Designer.cs b/shadowsocks-csharp/Properties/Resources.Designer.cs deleted file mode 100644 index e3cd3ddf4..000000000 --- a/shadowsocks-csharp/Properties/Resources.Designer.cs +++ /dev/null @@ -1,255 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Shadowsocks.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Shadowsocks.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to /* eslint-disable */ - ///// Was generated by gfwlist2pac in precise mode - ///// https://github.com/clowwindy/gfwlist2pac - /// - ///// 2019-10-06: More 'javascript' way to interaction with main program - ///// 2019-02-08: Updated to support shadowsocks-windows user rules. - /// - ///var proxy = __PROXY__; - ///var userrules = []; - ///var rules = []; - /// - ///// convert to abp grammar - ///var re = /^(@@)?\|\|.*?[^\^]$/; - ///for (var i = 0; i < __RULES__.length; i++) { - /// var s = __RULES__[i]; - /// if (s.match(re)) s += "^"; - /// rules.push(s); - ///} - /// - /// [rest of string was truncated]";. - /// - public static string abp_js { - get { - return ResourceManager.GetString("abp_js", resourceCulture); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - public static byte[] dlc_dat { - get { - object obj = ResourceManager.GetObject("dlc_dat", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized string similar to en,ru-RU,zh-CN,zh-TW,ja,ko,fr - ///#Restart program to apply translation,,,,,, - ///#This is comment line,,,,,, - ///#Always keep language name at head of file,,,,,, - ///#Language name is output in log,,,,,, - ///"#You can find it by search ""Current language is:""",,,,,, - ///#Please use UTF-8 with BOM encoding so we can edit it in Excel,,,,,, - ///,,,,,, - ///Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks,Shadowsocks - ///,,,,,, - ///#Menu,,,,,, - ///,,,,,, - ///System Proxy,Системный прокси-сервер,系统代理,系統代理,システムプロキシ,시스템 프록시,P [rest of string was truncated]";. - /// - public static string i18n_csv { - get { - return ResourceManager.GetString("i18n_csv", resourceCulture); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - public static byte[] libsscrypto_dll { - get { - object obj = ResourceManager.GetObject("libsscrypto_dll", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> - ///<!-- Warning: Configuration may reset after shadowsocks upgrade. --> - ///<!-- If you messed it up, delete this file and Shadowsocks will create a new one. --> - ///<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - /// <targets> - /// <!-- This line is managed by Shadowsocks. Do not modify it unless you know what you are doing.--> - /// <target name="file" xsi:type="File" fileName="ss_win_temp\shadowsocks.log" writ [rest of string was truncated]";. - /// - public static string NLog_config { - get { - return ResourceManager.GetString("NLog_config", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to listen-address __PRIVOXY_BIND_IP__:__PRIVOXY_BIND_PORT__ - ///toggle 0 - ///logfile ss_privoxy.log - ///show-on-task-bar 0 - ///activity-animation 0 - ///forward-socks5 / __SOCKS_HOST__:__SOCKS_PORT__ . - ///max-client-connections 2048 - ///hide-console - ///. - /// - public static string privoxy_conf { - get { - return ResourceManager.GetString("privoxy_conf", resourceCulture); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - public static byte[] privoxy_exe { - get { - object obj = ResourceManager.GetObject("privoxy_exe", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - public static System.Drawing.Bitmap ss32Fill { - get { - object obj = ResourceManager.GetObject("ss32Fill", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - public static System.Drawing.Bitmap ss32In { - get { - object obj = ResourceManager.GetObject("ss32In", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - public static System.Drawing.Bitmap ss32Out { - get { - object obj = ResourceManager.GetObject("ss32Out", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - public static System.Drawing.Bitmap ss32Outline { - get { - object obj = ResourceManager.GetObject("ss32Outline", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - public static System.Drawing.Bitmap ssw128 { - get { - object obj = ResourceManager.GetObject("ssw128", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - public static byte[] sysproxy_exe { - get { - object obj = ResourceManager.GetObject("sysproxy_exe", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - public static byte[] sysproxy64_exe { - get { - object obj = ResourceManager.GetObject("sysproxy64_exe", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized string similar to ! Put user rules line by line in this file. - ///! See https://adblockplus.org/en/filter-cheatsheet - ///. - /// - public static string user_rule { - get { - return ResourceManager.GetString("user_rule", resourceCulture); - } - } - } -} diff --git a/shadowsocks-csharp/Properties/Resources.resx b/shadowsocks-csharp/Properties/Resources.resx deleted file mode 100755 index 94f4f48e0..000000000 --- a/shadowsocks-csharp/Properties/Resources.resx +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Data\abp.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;gb2312 - - - ..\data\dlc.dat;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Data\i18n.csv;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\Data\libsscrypto.dll.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\data\nlog.config;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\data\privoxy_conf.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - - ..\data\privoxy.exe.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Resources\ss32Fill.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\ss32In.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\ss32Out.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\ss32Outline.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\ssw128.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Data\sysproxy64.exe.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Data\sysproxy.exe.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\data\user-rule.txt;System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - \ No newline at end of file diff --git a/shadowsocks-csharp/Properties/Settings.Designer.cs b/shadowsocks-csharp/Properties/Settings.Designer.cs deleted file mode 100644 index dfd4d4e0e..000000000 --- a/shadowsocks-csharp/Properties/Settings.Designer.cs +++ /dev/null @@ -1,86 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Shadowsocks.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("600")] - public int LogViewerWidth { - get { - return ((int)(this["LogViewerWidth"])); - } - set { - this["LogViewerWidth"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("400")] - public int LogViewerHeight { - get { - return ((int)(this["LogViewerHeight"])); - } - set { - this["LogViewerHeight"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool LogViewerMaximized { - get { - return ((bool)(this["LogViewerMaximized"])); - } - set { - this["LogViewerMaximized"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public int LogViewerTop { - get { - return ((int)(this["LogViewerTop"])); - } - set { - this["LogViewerTop"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] - public int LogViewerLeft { - get { - return ((int)(this["LogViewerLeft"])); - } - set { - this["LogViewerLeft"] = value; - } - } - } -} diff --git a/shadowsocks-csharp/Properties/Settings.settings b/shadowsocks-csharp/Properties/Settings.settings deleted file mode 100644 index e6469363b..000000000 --- a/shadowsocks-csharp/Properties/Settings.settings +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - 600 - - - 400 - - - True - - - 0 - - - 0 - - - \ No newline at end of file diff --git a/shadowsocks-csharp/Proxy/HttpProxy.cs b/shadowsocks-csharp/Proxy/HttpProxy.cs deleted file mode 100644 index b7580f04b..000000000 --- a/shadowsocks-csharp/Proxy/HttpProxy.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Util.Sockets; - -namespace Shadowsocks.Proxy -{ - public class HttpProxy : IProxy - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private class FakeAsyncResult : IAsyncResult - { - public readonly HttpState innerState; - - private readonly IAsyncResult r; - - public FakeAsyncResult(IAsyncResult orig, HttpState state) - { - r = orig; - innerState = state; - } - - public bool IsCompleted => r.IsCompleted; - public WaitHandle AsyncWaitHandle => r.AsyncWaitHandle; - public object AsyncState => innerState.AsyncState; - public bool CompletedSynchronously => r.CompletedSynchronously; - } - - private class HttpState - { - - public AsyncCallback Callback { get; set; } - - public object AsyncState { get; set; } - - public Exception ex { get; set; } - } - - public EndPoint LocalEndPoint => _remote.LocalEndPoint; - public EndPoint ProxyEndPoint { get; private set; } - public EndPoint DestEndPoint { get; private set; } - - - private readonly WrappedSocket _remote = new WrappedSocket(); - - - public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state) - { - ProxyEndPoint = remoteEP; - - _remote.BeginConnect(remoteEP, callback, state); - } - - public void EndConnectProxy(IAsyncResult asyncResult) - { - _remote.EndConnect(asyncResult); - _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - } - - private const string HTTP_CRLF = "\r\n"; - private const string HTTP_CONNECT_TEMPLATE = - "CONNECT {0} HTTP/1.1" + HTTP_CRLF + - "Host: {0}" + HTTP_CRLF + - "Proxy-Connection: keep-alive" + HTTP_CRLF + - "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36" + HTTP_CRLF + - "{1}" + // Proxy-Authorization if any - "" + HTTP_CRLF; // End with an empty line - private const string PROXY_AUTH_TEMPLATE = "Proxy-Authorization: Basic {0}" + HTTP_CRLF; - - public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null) - { - DestEndPoint = destEndPoint; - String authInfo = ""; - if (auth != null) - { - string authKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(auth.UserName + ":" + auth.Password)); - authInfo = string.Format(PROXY_AUTH_TEMPLATE, authKey); - } - string request = string.Format(HTTP_CONNECT_TEMPLATE, destEndPoint, authInfo); - - var b = Encoding.UTF8.GetBytes(request); - - var st = new HttpState(); - st.Callback = callback; - st.AsyncState = state; - - _remote.BeginSend(b, 0, b.Length, 0, HttpRequestSendCallback, st); - } - - public void EndConnectDest(IAsyncResult asyncResult) - { - var state = ((FakeAsyncResult)asyncResult).innerState; - - if (state.ex != null) - { - throw state.ex; - } - } - - public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, - object state) - { - _remote.BeginSend(buffer, offset, size, socketFlags, callback, state); - } - - public int EndSend(IAsyncResult asyncResult) - { - return _remote.EndSend(asyncResult); - } - - public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, - object state) - { - _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state); - } - - public int EndReceive(IAsyncResult asyncResult) - { - return _remote.EndReceive(asyncResult); - } - - public void Shutdown(SocketShutdown how) - { - _remote.Shutdown(how); - } - - public void Close() - { - _remote.Dispose(); - } - - private void HttpRequestSendCallback(IAsyncResult ar) - { - var state = (HttpState) ar.AsyncState; - try - { - _remote.EndSend(ar); - - // start line read - new LineReader(_remote, OnLineRead, OnException, OnFinish, Encoding.UTF8, HTTP_CRLF, 1024, new FakeAsyncResult(ar, state)); - } - catch (Exception ex) - { - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - - private void OnFinish(byte[] lastBytes, int index, int length, object state) - { - var st = (FakeAsyncResult)state; - - if (st.innerState.ex == null) - { - if (!_established) - { - st.innerState.ex = new Exception(I18N.GetString("Proxy request failed")); - } - // TODO: save last bytes - } - st.innerState.Callback?.Invoke(st); - } - - private void OnException(Exception ex, object state) - { - var st = (FakeAsyncResult) state; - - st.innerState.ex = ex; - } - - private static readonly Regex HttpRespondHeaderRegex = new Regex(@"^(HTTP/1\.\d) (\d{3}) (.+)$", RegexOptions.Compiled); - private int _respondLineCount = 0; - private bool _established = false; - - private bool OnLineRead(string line, object state) - { - logger.Trace(line); - - if (_respondLineCount == 0) - { - var m = HttpRespondHeaderRegex.Match(line); - if (m.Success) - { - var resultCode = m.Groups[2].Value; - if ("200" != resultCode) - { - return true; - } - _established = true; - } - } - else - { - if (string.IsNullOrEmpty(line)) - { - return true; - } - } - _respondLineCount++; - - return false; - } - } -} diff --git a/shadowsocks-csharp/Proxy/Socks5Proxy.cs b/shadowsocks-csharp/Proxy/Socks5Proxy.cs deleted file mode 100644 index 2fe294dfc..000000000 --- a/shadowsocks-csharp/Proxy/Socks5Proxy.cs +++ /dev/null @@ -1,323 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using Shadowsocks.Controller; -using Shadowsocks.Util.Sockets; - -namespace Shadowsocks.Proxy -{ - public class Socks5Proxy : IProxy - { - private class FakeAsyncResult : IAsyncResult - { - public readonly Socks5State innerState; - - private readonly IAsyncResult r; - - public FakeAsyncResult(IAsyncResult orig, Socks5State state) - { - r = orig; - innerState = state; - } - - public bool IsCompleted => r.IsCompleted; - public WaitHandle AsyncWaitHandle => r.AsyncWaitHandle; - public object AsyncState => innerState.AsyncState; - public bool CompletedSynchronously => r.CompletedSynchronously; - } - - private class Socks5State - { - public AsyncCallback Callback { get; set; } - - public object AsyncState { get; set; } - - public int BytesToRead; - - public Exception ex { get; set; } - } - - private readonly WrappedSocket _remote = new WrappedSocket(); - - private const int Socks5PktMaxSize = 4 + 16 + 2; - private readonly byte[] _receiveBuffer = new byte[Socks5PktMaxSize]; - - public EndPoint LocalEndPoint => _remote.LocalEndPoint; - public EndPoint ProxyEndPoint { get; private set; } - public EndPoint DestEndPoint { get; private set; } - - public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state) - { - var st = new Socks5State(); - st.Callback = callback; - st.AsyncState = state; - - ProxyEndPoint = remoteEP; - - _remote.BeginConnect(remoteEP, ConnectCallback, st); - } - - public void EndConnectProxy(IAsyncResult asyncResult) - { - var state = ((FakeAsyncResult)asyncResult).innerState; - - if (state.ex != null) - { - throw state.ex; - } - } - - public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null) - { - // TODO: support SOCKS5 auth - DestEndPoint = destEndPoint; - - byte[] request = null; - byte atyp = 0; - int port; - - var dep = destEndPoint as DnsEndPoint; - if (dep != null) - { - // is a domain name, we will leave it to server - - atyp = 3; // DOMAINNAME - var enc = Encoding.UTF8; - var hostByteCount = enc.GetByteCount(dep.Host); - - request = new byte[4 + 1/*length byte*/ + hostByteCount + 2]; - request[4] = (byte)hostByteCount; - enc.GetBytes(dep.Host, 0, dep.Host.Length, request, 5); - - port = dep.Port; - } - else - { - switch (DestEndPoint.AddressFamily) - { - case AddressFamily.InterNetwork: - request = new byte[4 + 4 + 2]; - atyp = 1; // IP V4 address - break; - case AddressFamily.InterNetworkV6: - request = new byte[4 + 16 + 2]; - atyp = 4; // IP V6 address - break; - default: - throw new Exception(I18N.GetString("Proxy request failed")); - } - port = ((IPEndPoint) DestEndPoint).Port; - var addr = ((IPEndPoint)DestEndPoint).Address.GetAddressBytes(); - Array.Copy(addr, 0, request, 4, request.Length - 4 - 2); - } - - // 构造request包剩余部分 - request[0] = 5; - request[1] = 1; - request[2] = 0; - request[3] = atyp; - request[request.Length - 2] = (byte) ((port >> 8) & 0xff); - request[request.Length - 1] = (byte) (port & 0xff); - - var st = new Socks5State(); - st.Callback = callback; - st.AsyncState = state; - - _remote.BeginSend(request, 0, request.Length, 0, Socks5RequestSendCallback, st); - } - - public void EndConnectDest(IAsyncResult asyncResult) - { - var state = ((FakeAsyncResult)asyncResult).innerState; - - if (state.ex != null) - { - throw state.ex; - } - } - - public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, - object state) - { - _remote.BeginSend(buffer, offset, size, socketFlags, callback, state); - } - - public int EndSend(IAsyncResult asyncResult) - { - return _remote.EndSend(asyncResult); - } - - public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, - object state) - { - _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state); - } - - public int EndReceive(IAsyncResult asyncResult) - { - return _remote.EndReceive(asyncResult); - } - - public void Shutdown(SocketShutdown how) - { - _remote.Shutdown(how); - } - - public void Close() - { - _remote.Dispose(); - } - - - private void ConnectCallback(IAsyncResult ar) - { - var state = (Socks5State) ar.AsyncState; - try - { - _remote.EndConnect(ar); - - _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - - byte[] handshake = {5, 1, 0}; - _remote.BeginSend(handshake, 0, handshake.Length, 0, Socks5HandshakeSendCallback, state); - } - catch (Exception ex) - { - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - - private void Socks5HandshakeSendCallback(IAsyncResult ar) - { - var state = (Socks5State)ar.AsyncState; - try - { - _remote.EndSend(ar); - - _remote.BeginReceive(_receiveBuffer, 0, 2, 0, Socks5HandshakeReceiveCallback, state); - } - catch (Exception ex) - { - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - - private void Socks5HandshakeReceiveCallback(IAsyncResult ar) - { - Exception ex = null; - var state = (Socks5State)ar.AsyncState; - try - { - var bytesRead = _remote.EndReceive(ar); - if (bytesRead >= 2) - { - if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0) - { - ex = new Exception(I18N.GetString("Proxy handshake failed")); - } - } - else - { - ex = new Exception(I18N.GetString("Proxy handshake failed")); - } - } - catch (Exception ex2) - { - ex = ex2; - } - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - - - private void Socks5RequestSendCallback(IAsyncResult ar) - { - var state = (Socks5State)ar.AsyncState; - try - { - _remote.EndSend(ar); - - _remote.BeginReceive(_receiveBuffer, 0, 4, 0, Socks5ReplyReceiveCallback, state); - } - catch (Exception ex) - { - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - - private void Socks5ReplyReceiveCallback(IAsyncResult ar) - { - var state = (Socks5State)ar.AsyncState; - try - { - var bytesRead = _remote.EndReceive(ar); - if (bytesRead >= 4) - { - if (_receiveBuffer[0] == 5 && _receiveBuffer[1] == 0) - { - // 跳过剩下的reply - switch (_receiveBuffer[3]) // atyp - { - case 1: - state.BytesToRead = 4 + 2; - _remote.BeginReceive(_receiveBuffer, 0, 4 + 2, 0, Socks5ReplyReceiveCallback2, state); - break; - case 4: - state.BytesToRead = 16 + 2; - _remote.BeginReceive(_receiveBuffer, 0, 16 + 2, 0, Socks5ReplyReceiveCallback2, state); - break; - default: - state.ex = new Exception(I18N.GetString("Proxy request failed")); - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - break; - } - } - else - { - state.ex = new Exception(I18N.GetString("Proxy request failed")); - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - else - { - state.ex = new Exception(I18N.GetString("Proxy request failed")); - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - catch (Exception ex) - { - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } - - - private void Socks5ReplyReceiveCallback2(IAsyncResult ar) - { - Exception ex = null; - var state = (Socks5State)ar.AsyncState; - try - { - var bytesRead = _remote.EndReceive(ar); - var bytesNeedSkip = state.BytesToRead; - - if (bytesRead < bytesNeedSkip) - { - ex = new Exception(I18N.GetString("Proxy request failed")); - } - } - catch (Exception ex2) - { - ex = ex2; - } - - state.ex = ex; - state.Callback?.Invoke(new FakeAsyncResult(ar, state)); - } - } -} diff --git a/shadowsocks-csharp/Resources/ss128.pdn b/shadowsocks-csharp/Resources/ss128.pdn deleted file mode 100644 index 5fa50a374..000000000 Binary files a/shadowsocks-csharp/Resources/ss128.pdn and /dev/null differ diff --git a/shadowsocks-csharp/Resources/ss32.pdn b/shadowsocks-csharp/Resources/ss32.pdn deleted file mode 100644 index 13a42ac40..000000000 Binary files a/shadowsocks-csharp/Resources/ss32.pdn and /dev/null differ diff --git a/shadowsocks-csharp/Resources/ss32In.png b/shadowsocks-csharp/Resources/ss32In.png deleted file mode 100644 index d3e0aa911..000000000 Binary files a/shadowsocks-csharp/Resources/ss32In.png and /dev/null differ diff --git a/shadowsocks-csharp/Resources/ss32Out.png b/shadowsocks-csharp/Resources/ss32Out.png deleted file mode 100644 index 6f6b4cb82..000000000 Binary files a/shadowsocks-csharp/Resources/ss32Out.png and /dev/null differ diff --git a/shadowsocks-csharp/Resources/ssw128.png b/shadowsocks-csharp/Resources/ssw128.png deleted file mode 100755 index f9676a25f..000000000 Binary files a/shadowsocks-csharp/Resources/ssw128.png and /dev/null differ diff --git a/shadowsocks-csharp/Settings.cs b/shadowsocks-csharp/Settings.cs deleted file mode 100644 index a1663c6dd..000000000 --- a/shadowsocks-csharp/Settings.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Shadowsocks.Properties { - - - // 通过此类可以处理设置类的特定事件: - // 在更改某个设置的值之前将引发 SettingChanging 事件。 - // 在更改某个设置的值之后将引发 PropertyChanged 事件。 - // 在加载设置值之后将引发 SettingsLoaded 事件。 - // 在保存设置值之前将引发 SettingsSaving 事件。 - internal sealed partial class Settings { - - public Settings() { - // // 若要为保存和更改设置添加事件处理程序,请取消注释下列行: - // - // this.SettingChanging += this.SettingChangingEventHandler; - // - // this.SettingsSaving += this.SettingsSavingEventHandler; - // - } - - private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { - // 在此处添加用于处理 SettingChangingEvent 事件的代码。 - } - - private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { - // 在此处添加用于处理 SettingsSaving 事件的代码。 - } - } -} diff --git a/shadowsocks-csharp/Util/ProcessManagement/Job.cs b/shadowsocks-csharp/Util/ProcessManagement/Job.cs deleted file mode 100644 index 0dadbdbca..000000000 --- a/shadowsocks-csharp/Util/ProcessManagement/Job.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using NLog; -using Shadowsocks.Controller; - -namespace Shadowsocks.Util.ProcessManagement -{ - /* - * See: - * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net - */ - public class Job : IDisposable - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private IntPtr handle = IntPtr.Zero; - - public Job() - { - handle = CreateJobObject(IntPtr.Zero, null); - var extendedInfoPtr = IntPtr.Zero; - var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION - { - LimitFlags = 0x2000 - }; - - var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - BasicLimitInformation = info - }; - - try - { - int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); - extendedInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, - (uint) length)) - throw new Exception(string.Format("Unable to set information. Error: {0}", - Marshal.GetLastWin32Error())); - } - finally - { - if (extendedInfoPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(extendedInfoPtr); - extendedInfoPtr = IntPtr.Zero; - } - } - } - - public bool AddProcess(IntPtr processHandle) - { - var succ = AssignProcessToJobObject(handle, processHandle); - - if (!succ) - { - logger.Error("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error()); - } - - return succ; - } - - public bool AddProcess(int processId) - { - return AddProcess(Process.GetProcessById(processId).Handle); - } - - #region IDisposable - - private bool disposed; - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) return; - disposed = true; - - if (disposing) - { - // no managed objects to free - } - - if (handle != IntPtr.Zero) - { - CloseHandle(handle); - handle = IntPtr.Zero; - } - } - - ~Job() - { - Dispose(false); - } - - #endregion - - #region Interop - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr CreateJobObject(IntPtr a, string lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, UInt32 cbJobObjectInfoLength); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CloseHandle(IntPtr hObject); - - #endregion - } - - #region Helper classes - - [StructLayout(LayoutKind.Sequential)] - struct IO_COUNTERS - { - public UInt64 ReadOperationCount; - public UInt64 WriteOperationCount; - public UInt64 OtherOperationCount; - public UInt64 ReadTransferCount; - public UInt64 WriteTransferCount; - public UInt64 OtherTransferCount; - } - - - [StructLayout(LayoutKind.Sequential)] - struct JOBOBJECT_BASIC_LIMIT_INFORMATION - { - public Int64 PerProcessUserTimeLimit; - public Int64 PerJobUserTimeLimit; - public UInt32 LimitFlags; - public UIntPtr MinimumWorkingSetSize; - public UIntPtr MaximumWorkingSetSize; - public UInt32 ActiveProcessLimit; - public UIntPtr Affinity; - public UInt32 PriorityClass; - public UInt32 SchedulingClass; - } - - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public UInt32 nLength; - public IntPtr lpSecurityDescriptor; - public Int32 bInheritHandle; - } - - [StructLayout(LayoutKind.Sequential)] - struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; - public IO_COUNTERS IoInfo; - public UIntPtr ProcessMemoryLimit; - public UIntPtr JobMemoryLimit; - public UIntPtr PeakProcessMemoryUsed; - public UIntPtr PeakJobMemoryUsed; - } - - public enum JobObjectInfoType - { - AssociateCompletionPortInformation = 7, - BasicLimitInformation = 2, - BasicUIRestrictions = 4, - EndOfJobTimeInformation = 6, - ExtendedLimitInformation = 9, - SecurityLimitInformation = 5, - GroupInformation = 11 - } - - #endregion -} diff --git a/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs b/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs deleted file mode 100644 index 9421638f2..000000000 --- a/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Diagnostics; -using System.Management; -using System.Text; - -namespace Shadowsocks.Util.ProcessManagement -{ - static class ThreadUtil - { - - /* - * See: - * http://stackoverflow.com/questions/2633628/can-i-get-command-line-arguments-of-other-processes-from-net-c - */ - public static string GetCommandLine(this Process process) - { - var commandLine = new StringBuilder(process.MainModule.FileName); - - commandLine.Append(" "); - using (var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id)) - { - foreach (var @object in searcher.Get()) - { - commandLine.Append(@object["CommandLine"]); - commandLine.Append(" "); - } - } - - return commandLine.ToString(); - } - } -} diff --git a/shadowsocks-csharp/Util/Sockets/LineReader.cs b/shadowsocks-csharp/Util/Sockets/LineReader.cs deleted file mode 100644 index 93626371b..000000000 --- a/shadowsocks-csharp/Util/Sockets/LineReader.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System; -using System.Text; - -namespace Shadowsocks.Util.Sockets -{ - public class LineReader - { - private readonly WrappedSocket _socket; - private readonly Func _onLineRead; - private readonly Action _onException; - private readonly Action _onFinish; - private readonly Encoding _encoding; - // private readonly string _delimiter; - private readonly byte[] _delimiterBytes; - private readonly int[] _delimiterSearchCharTable; - private readonly int[] _delimiterSearchOffsetTable; - private readonly object _state; - - private readonly byte[] _lineBuffer; - - private int _bufferIndex; - - public LineReader(WrappedSocket socket, Func onLineRead, Action onException, - Action onFinish, Encoding encoding, string delimiter, int maxLineBytes, object state) - { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - if (onLineRead == null) - { - throw new ArgumentNullException(nameof(onLineRead)); - } - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } - if (delimiter == null) - { - throw new ArgumentNullException(nameof(delimiter)); - } - - _socket = socket; - _onLineRead = onLineRead; - _onException = onException; - _onFinish = onFinish; - _encoding = encoding; - // _delimiter = delimiter; - _state = state; - - // decode delimiter - _delimiterBytes = encoding.GetBytes(delimiter); - - if (_delimiterBytes.Length == 0) - { - throw new ArgumentException("Too short!", nameof(delimiter)); - } - - if (maxLineBytes < _delimiterBytes.Length) - { - throw new ArgumentException("Too small!", nameof(maxLineBytes)); - } - - _delimiterSearchCharTable = MakeCharTable(_delimiterBytes); - _delimiterSearchOffsetTable = MakeOffsetTable(_delimiterBytes); - - _lineBuffer = new byte[maxLineBytes]; - - // start reading - socket.BeginReceive(_lineBuffer, 0, maxLineBytes, 0, ReceiveCallback, 0); - } - - private void ReceiveCallback(IAsyncResult ar) - { - int length = (int)ar.AsyncState; - try - { - var bytesRead = _socket.EndReceive(ar); - - if (bytesRead == 0) - { - OnFinish(length); - return; - } - - length += bytesRead; - - int i; - while ((i = IndexOf(_lineBuffer, _bufferIndex, length, _delimiterBytes, _delimiterSearchOffsetTable, - _delimiterSearchCharTable)) != -1) - { - var decodeLen = i - _bufferIndex; - string line = _encoding.GetString(_lineBuffer, _bufferIndex, decodeLen); - - _bufferIndex = i + _delimiterBytes.Length; - length -= decodeLen; - length -= _delimiterBytes.Length; - - var stop = _onLineRead(line, _state); - if (stop) - { - OnFinish(length); - return; - } - } - if (length == _lineBuffer.Length) - { - OnException(new IndexOutOfRangeException("LineBuffer full! Try increace maxLineBytes!")); - OnFinish(length); - - return; - } - - if (_bufferIndex > 0) - { - Buffer.BlockCopy(_lineBuffer, _bufferIndex, _lineBuffer, 0, length); - _bufferIndex = 0; - } - - _socket.BeginReceive(_lineBuffer, length, _lineBuffer.Length - length, 0, ReceiveCallback, length); - } - catch (Exception ex) - { - OnException(ex); - OnFinish(length); - } - } - - private void OnException(Exception ex) - { - _onException?.Invoke(ex, _state); - } - - private void OnFinish(int length) - { - _onFinish?.Invoke(_lineBuffer, _bufferIndex, length, _state); - } - - #region Boyer-Moore string search - - public static int IndexOf(byte[] haystack, int index, int length, byte[] needle, int[] offsetTable, int[] charTable) - { - var end = index + length; - for (int i = needle.Length - 1 + index, j; i < end;) - { - for (j = needle.Length - 1; needle[j] == haystack[i]; --i, --j) - { - if (j == 0) - { - return i; - } - } - // i += needle.length - j; // For naive method - i += Math.Max(offsetTable[needle.Length - 1 - j], charTable[haystack[i]]); - } - return -1; - } - - /** - * Makes the jump table based on the mismatched character information. - */ - private static int[] MakeCharTable(byte[] needle) - { - const int ALPHABET_SIZE = 256; - int[] table = new int[ALPHABET_SIZE]; - for (int i = 0; i < table.Length; ++i) - { - table[i] = needle.Length; - } - for (int i = 0; i < needle.Length - 1; ++i) - { - table[needle[i]] = needle.Length - 1 - i; - } - return table; - } - - /** - * Makes the jump table based on the scan offset which mismatch occurs. - */ - private static int[] MakeOffsetTable(byte[] needle) - { - int[] table = new int[needle.Length]; - int lastPrefixPosition = needle.Length; - for (int i = needle.Length - 1; i >= 0; --i) - { - if (IsPrefix(needle, i + 1)) - { - lastPrefixPosition = i + 1; - } - table[needle.Length - 1 - i] = lastPrefixPosition - i + needle.Length - 1; - } - for (int i = 0; i < needle.Length - 1; ++i) - { - int slen = SuffixLength(needle, i); - table[slen] = needle.Length - 1 - i + slen; - } - return table; - } - - /** - * Is needle[p:end] a prefix of needle? - */ - private static bool IsPrefix(byte[] needle, int p) - { - for (int i = p, j = 0; i < needle.Length; ++i, ++j) - { - if (needle[i] != needle[j]) - { - return false; - } - } - return true; - } - - /** - * Returns the maximum length of the substring ends at p and is a suffix. - */ - private static int SuffixLength(byte[] needle, int p) - { - int len = 0; - for (int i = p, j = needle.Length - 1; - i >= 0 && needle[i] == needle[j]; --i, --j) - { - len += 1; - } - return len; - } - - #endregion - } -} diff --git a/shadowsocks-csharp/Util/SystemProxy/ProxyException.cs b/shadowsocks-csharp/Util/SystemProxy/ProxyException.cs deleted file mode 100644 index 76e858cbd..000000000 --- a/shadowsocks-csharp/Util/SystemProxy/ProxyException.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace Shadowsocks.Util.SystemProxy -{ - enum ProxyExceptionType - { - Unspecific, - FailToRun, - QueryReturnEmpty, - SysproxyExitError, - QueryReturnMalformed - } - - class ProxyException : Exception - { - // provide more specific information about exception - public ProxyExceptionType Type { get; } - - public ProxyException() - { - } - - public ProxyException(string message) : base(message) - { - } - - public ProxyException(string message, Exception innerException) : base(message, innerException) - { - } - - protected ProxyException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - public ProxyException(ProxyExceptionType type) - { - this.Type = type; - } - - public ProxyException(ProxyExceptionType type, string message) : base(message) - { - this.Type = type; - } - - public ProxyException(ProxyExceptionType type, string message, Exception innerException) : base(message, innerException) - { - this.Type = type; - } - - protected ProxyException(ProxyExceptionType type, SerializationInfo info, StreamingContext context) : base(info, context) - { - this.Type = type; - } - } -} diff --git a/shadowsocks-csharp/Util/SystemProxy/Sysproxy.cs b/shadowsocks-csharp/Util/SystemProxy/Sysproxy.cs deleted file mode 100644 index 220576f6b..000000000 --- a/shadowsocks-csharp/Util/SystemProxy/Sysproxy.cs +++ /dev/null @@ -1,291 +0,0 @@ -using Newtonsoft.Json; -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; - -namespace Shadowsocks.Util.SystemProxy -{ - public static class Sysproxy - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private const string _userWininetConfigFile = "user-wininet.json"; - - private readonly static string[] _lanIP = { - "", - "localhost", - "127.*", - "10.*", - "172.16.*", - "172.17.*", - "172.18.*", - "172.19.*", - "172.20.*", - "172.21.*", - "172.22.*", - "172.23.*", - "172.24.*", - "172.25.*", - "172.26.*", - "172.27.*", - "172.28.*", - "172.29.*", - "172.30.*", - "172.31.*", - "192.168.*" - }; - - - private static string _queryStr; - - // In general, this won't change - // format: - // - // - // - // - private static SysproxyConfig _userSettings = null; - - enum RET_ERRORS : int - { - RET_NO_ERROR = 0, - INVALID_FORMAT = 1, - NO_PERMISSION = 2, - SYSCALL_FAILED = 3, - NO_MEMORY = 4, - INVAILD_OPTION_COUNT = 5, - }; - - static Sysproxy() - { - try - { - FileManager.UncompressFile(Utils.GetTempPath("sysproxy.exe"), - Environment.Is64BitOperatingSystem ? Resources.sysproxy64_exe : Resources.sysproxy_exe); - } - catch (IOException e) - { - logger.LogUsefulException(e); - } - } - - public static void SetIEProxy(bool enable, bool global, string proxyServer, string pacURL) - { - Read(); - - if (!_userSettings.UserSettingsRecorded) - { - // record user settings - ExecSysproxy("query"); - ParseQueryStr(_queryStr); - } - - string arguments; - if (enable) - { - string customBypassString = _userSettings.BypassList ?? ""; - List customBypassList = new List(customBypassString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)); - customBypassList.AddRange(_lanIP); - string[] realBypassList = customBypassList.Distinct().ToArray(); - string realBypassString = string.Join(";", realBypassList); - - arguments = global - ? $"global {proxyServer} {realBypassString}" - : $"pac {pacURL}"; - } - else - { - // restore user settings - var flags = _userSettings.Flags; - var proxy_server = _userSettings.ProxyServer ?? "-"; - var bypass_list = _userSettings.BypassList ?? "-"; - var pac_url = _userSettings.PacUrl ?? "-"; - arguments = $"set {flags} {proxy_server} {bypass_list} {pac_url}"; - - // have to get new settings - _userSettings.UserSettingsRecorded = false; - } - - Save(); - ExecSysproxy(arguments); - } - - // set system proxy to 1 (null) (null) (null) - public static bool ResetIEProxy() - { - try - { - // clear user-wininet.json - _userSettings = new SysproxyConfig(); - Save(); - // clear system setting - ExecSysproxy("set 1 - - -"); - } - catch (Exception) - { - return false; - } - - return true; - } - - private static void ExecSysproxy(string arguments) - { - // using event to avoid hanging when redirect standard output/error - // ref: https://stackoverflow.com/questions/139593/processstartinfo-hanging-on-waitforexit-why - // and http://blog.csdn.net/zhangweixing0/article/details/7356841 - using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) - using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) - { - using (var process = new Process()) - { - // Configure the process using the StartInfo properties. - process.StartInfo.FileName = Utils.GetTempPath("sysproxy.exe"); - process.StartInfo.Arguments = arguments; - process.StartInfo.WorkingDirectory = Utils.GetTempPath(); - process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.RedirectStandardOutput = true; - - // Need to provide encoding info, or output/error strings we got will be wrong. - process.StartInfo.StandardOutputEncoding = Encoding.Unicode; - process.StartInfo.StandardErrorEncoding = Encoding.Unicode; - - process.StartInfo.CreateNoWindow = true; - - StringBuilder output = new StringBuilder(); - StringBuilder error = new StringBuilder(); - - process.OutputDataReceived += (sender, e) => - { - if (e.Data == null) - { - outputWaitHandle.Set(); - } - else - { - output.AppendLine(e.Data); - } - }; - process.ErrorDataReceived += (sender, e) => - { - if (e.Data == null) - { - errorWaitHandle.Set(); - } - else - { - error.AppendLine(e.Data); - } - }; - try - { - process.Start(); - - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - - process.WaitForExit(); - } - catch (System.ComponentModel.Win32Exception e) - { - // log the arguments - throw new ProxyException(ProxyExceptionType.FailToRun, process.StartInfo.Arguments, e); - } - var stderr = error.ToString(); - var stdout = output.ToString(); - - var exitCode = process.ExitCode; - if (exitCode != (int)RET_ERRORS.RET_NO_ERROR) - { - throw new ProxyException(ProxyExceptionType.SysproxyExitError, stderr); - } - - if (arguments == "query") - { - if (string.IsNullOrWhiteSpace(stdout)) - { - // we cannot get user settings - throw new ProxyException(ProxyExceptionType.QueryReturnEmpty); - } - _queryStr = stdout; - } - } - } - } - - private static void Save() - { - try - { - using (StreamWriter sw = new StreamWriter(File.Open(Utils.GetTempPath(_userWininetConfigFile), FileMode.Create))) - { - string jsonString = JsonConvert.SerializeObject(_userSettings, Formatting.Indented); - sw.Write(jsonString); - sw.Flush(); - } - } - catch (IOException e) - { - logger.LogUsefulException(e); - } - } - - private static void Read() - { - try - { - string configContent = File.ReadAllText(Utils.GetTempPath(_userWininetConfigFile)); - _userSettings = JsonConvert.DeserializeObject(configContent); - } - catch (Exception) - { - // Suppress all exceptions. finally block will initialize new user config settings. - } - finally - { - if (_userSettings == null) _userSettings = new SysproxyConfig(); - } - } - - private static void ParseQueryStr(string str) - { - string[] userSettingsArr = str.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); - - // sometimes sysproxy output in utf16le instead of ascii - // manually translate it - if (userSettingsArr.Length != 4) - { - byte[] strByte = Encoding.ASCII.GetBytes(str); - str = Encoding.Unicode.GetString(strByte); - userSettingsArr = str.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); - // still fail, throw exception with string hexdump - if (userSettingsArr.Length != 4) - { - throw new ProxyException(ProxyExceptionType.QueryReturnMalformed, BitConverter.ToString(strByte)); - } - } - - _userSettings.Flags = userSettingsArr[0]; - - // handle output from WinINET - if (userSettingsArr[1] == "(null)") _userSettings.ProxyServer = null; - else _userSettings.ProxyServer = userSettingsArr[1]; - if (userSettingsArr[2] == "(null)") _userSettings.BypassList = null; - else _userSettings.BypassList = userSettingsArr[2]; - if (userSettingsArr[3] == "(null)") _userSettings.PacUrl = null; - else _userSettings.PacUrl = userSettingsArr[3]; - - _userSettings.UserSettingsRecorded = true; - } - } -} diff --git a/shadowsocks-csharp/Util/Util.cs b/shadowsocks-csharp/Util/Util.cs deleted file mode 100755 index 9d8e80ce5..000000000 --- a/shadowsocks-csharp/Util/Util.cs +++ /dev/null @@ -1,305 +0,0 @@ -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Runtime.InteropServices; -using System.Windows.Forms; -using Microsoft.Win32; -using Shadowsocks.Controller; -using Shadowsocks.Model; -using System.Drawing; -using ZXing; -using ZXing.QrCode; -using ZXing.Common; - -namespace Shadowsocks.Util -{ - public struct BandwidthScaleInfo - { - public float value; - public string unitName; - public long unit; - - public BandwidthScaleInfo(float value, string unitName, long unit) - { - this.value = value; - this.unitName = unitName; - this.unit = unit; - } - } - - public static class Utils - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private static string _tempPath = null; - - // return path to store temporary files - public static string GetTempPath() - { - if (_tempPath == null) - { - bool isPortableMode = Configuration.Load().portableMode; - try - { - if (isPortableMode) - { - _tempPath = Directory.CreateDirectory("ss_win_temp").FullName; - // don't use "/", it will fail when we call explorer /select xxx/ss_win_temp\xxx.log - } - else - { - _tempPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), @"Shadowsocks\ss_win_temp_" + Program.ExecutablePath.GetHashCode())).FullName; - } - } - catch (Exception e) - { - logger.Error(e); - throw; - } - } - return _tempPath; - } - - public enum WindowsThemeMode { Dark, Light } - - // Support on Windows 10 1903+ - public static WindowsThemeMode GetWindows10SystemThemeSetting() - { - WindowsThemeMode themeMode = WindowsThemeMode.Dark; - try - { - RegistryKey reg_ThemesPersonalize = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", false); - if (reg_ThemesPersonalize.GetValue("SystemUsesLightTheme") != null) - { - if ((int)(reg_ThemesPersonalize.GetValue("SystemUsesLightTheme")) == 0) // 0:dark mode, 1:light mode - themeMode = WindowsThemeMode.Dark; - else - themeMode = WindowsThemeMode.Light; - } - else - { - throw new Exception("Reg-Value SystemUsesLightTheme not found."); - } - } - catch - { - - logger.Debug( - $"Cannot get Windows 10 system theme mode, return default value 0 (dark mode)."); - - } - return themeMode; - } - - // return a full path with filename combined which pointed to the temporary directory - public static string GetTempPath(string filename) - { - return Path.Combine(GetTempPath(), filename); - } - - public static string UnGzip(byte[] buf) - { - byte[] buffer = new byte[1024]; - int n; - using (MemoryStream sb = new MemoryStream()) - { - using (GZipStream input = new GZipStream(new MemoryStream(buf), - CompressionMode.Decompress, - false)) - { - while ((n = input.Read(buffer, 0, buffer.Length)) > 0) - { - sb.Write(buffer, 0, n); - } - } - return System.Text.Encoding.UTF8.GetString(sb.ToArray()); - } - } - - public static string FormatBandwidth(long n) - { - var result = GetBandwidthScale(n); - return $"{result.value:0.##}{result.unitName}"; - } - - public static string FormatBytes(long bytes) - { - const long K = 1024L; - const long M = K * 1024L; - const long G = M * 1024L; - const long T = G * 1024L; - const long P = T * 1024L; - const long E = P * 1024L; - - if (bytes >= P * 990) - return (bytes / (double)E).ToString("F5") + "EiB"; - if (bytes >= T * 990) - return (bytes / (double)P).ToString("F5") + "PiB"; - if (bytes >= G * 990) - return (bytes / (double)T).ToString("F5") + "TiB"; - if (bytes >= M * 990) - { - return (bytes / (double)G).ToString("F4") + "GiB"; - } - if (bytes >= M * 100) - { - return (bytes / (double)M).ToString("F1") + "MiB"; - } - if (bytes >= M * 10) - { - return (bytes / (double)M).ToString("F2") + "MiB"; - } - if (bytes >= K * 990) - { - return (bytes / (double)M).ToString("F3") + "MiB"; - } - if (bytes > K * 2) - { - return (bytes / (double)K).ToString("F1") + "KiB"; - } - return bytes.ToString() + "B"; - } - - /// - /// Return scaled bandwidth - /// - /// Raw bandwidth - /// - /// The BandwidthScaleInfo struct - /// - public static BandwidthScaleInfo GetBandwidthScale(long n) - { - long scale = 1; - float f = n; - string unit = "B"; - if (f > 1024) - { - f = f / 1024; - scale <<= 10; - unit = "KiB"; - } - if (f > 1024) - { - f = f / 1024; - scale <<= 10; - unit = "MiB"; - } - if (f > 1024) - { - f = f / 1024; - scale <<= 10; - unit = "GiB"; - } - if (f > 1024) - { - f = f / 1024; - scale <<= 10; - unit = "TiB"; - } - return new BandwidthScaleInfo(f, unit, scale); - } - - public static RegistryKey OpenRegKey(string name, bool writable, RegistryHive hive = RegistryHive.CurrentUser) - { - // we are building x86 binary for both x86 and x64, which will - // cause problem when opening registry key - // detect operating system instead of CPU - if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name)); - try - { - RegistryKey userKey = RegistryKey.OpenBaseKey(hive, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32) - .OpenSubKey(name, writable); - return userKey; - } - catch (ArgumentException ae) - { - MessageBox.Show("OpenRegKey: " + ae.ToString()); - return null; - } - catch (Exception e) - { - logger.LogUsefulException(e); - return null; - } - } - - public static bool IsWinVistaOrHigher() - { - return Environment.OSVersion.Version.Major > 5; - } - - public static string ScanQRCodeFromScreen() - { - foreach (Screen screen in Screen.AllScreens) - { - using (Bitmap fullImage = new Bitmap(screen.Bounds.Width, - screen.Bounds.Height)) - { - using (Graphics g = Graphics.FromImage(fullImage)) - { - g.CopyFromScreen(screen.Bounds.X, - screen.Bounds.Y, - 0, 0, - fullImage.Size, - CopyPixelOperation.SourceCopy); - } - int maxTry = 10; - for (int i = 0; i < maxTry; i++) - { - int marginLeft = (int)((double)fullImage.Width * i / 2.5 / maxTry); - int marginTop = (int)((double)fullImage.Height * i / 2.5 / maxTry); - Rectangle cropRect = new Rectangle(marginLeft, marginTop, fullImage.Width - marginLeft * 2, fullImage.Height - marginTop * 2); - Bitmap target = new Bitmap(screen.Bounds.Width, screen.Bounds.Height); - - double imageScale = (double)screen.Bounds.Width / (double)cropRect.Width; - using (Graphics g = Graphics.FromImage(target)) - { - g.DrawImage(fullImage, new Rectangle(0, 0, target.Width, target.Height), - cropRect, - GraphicsUnit.Pixel); - } - var source = new BitmapLuminanceSource(target); - var bitmap = new BinaryBitmap(new HybridBinarizer(source)); - QRCodeReader reader = new QRCodeReader(); - var result = reader.decode(bitmap); - if (result != null) - return result.Text; - } - } - } - return null; - } - - // See: https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx - public static bool IsSupportedRuntimeVersion() - { - /* - * +-----------------------------------------------------------------+----------------------------+ - * | Version | Value of the Release DWORD | - * +-----------------------------------------------------------------+----------------------------+ - * | .NET Framework 4.6.2 installed on Windows 10 Anniversary Update | 394802 | - * | .NET Framework 4.6.2 installed on all other Windows OS versions | 394806 | - * +-----------------------------------------------------------------+----------------------------+ - */ - const int minSupportedRelease = 394802; - - const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; - using (var ndpKey = OpenRegKey(subkey, false, RegistryHive.LocalMachine)) - { - if (ndpKey?.GetValue("Release") != null) - { - var releaseKey = (int)ndpKey.GetValue("Release"); - - if (releaseKey >= minSupportedRelease) - { - return true; - } - } - } - return false; - } - } -} diff --git a/shadowsocks-csharp/Util/ViewUtils.cs b/shadowsocks-csharp/Util/ViewUtils.cs deleted file mode 100644 index 43f547213..000000000 --- a/shadowsocks-csharp/Util/ViewUtils.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Shadowsocks.Controller; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Linq; -using System.Reflection; -using System.Windows.Forms; - -namespace Shadowsocks.Util -{ - public static class ViewUtils - { - public static IEnumerable GetChildControls(this Control control) where TControl : Control - { - if (control.Controls.Count == 0) - { - return Enumerable.Empty(); - } - var children = control.Controls.OfType().ToList(); - return children.SelectMany(GetChildControls).Concat(children); - } - - public static IEnumerable GetMenuItems(Menu m) - { - if (m?.MenuItems == null || m.MenuItems.Count == 0) return Enumerable.Empty(); - var children = new List(); - foreach (var item in m.MenuItems) - { - children.Add((MenuItem)item); - } - return children.SelectMany(GetMenuItems).Concat(children); - } - - // Workaround NotifyIcon's 63 chars limit - // https://stackoverflow.com/questions/579665/how-can-i-show-a-systray-tooltip-longer-than-63-chars - public static void SetNotifyIconText(NotifyIcon ni, string text) - { - if (text.Length >= 128) - throw new ArgumentOutOfRangeException("Text limited to 127 characters"); - Type t = typeof(NotifyIcon); - BindingFlags hidden = BindingFlags.NonPublic | BindingFlags.Instance; - t.GetField("text", hidden).SetValue(ni, text); - if ((bool)t.GetField("added", hidden).GetValue(ni)) - t.GetMethod("UpdateIcon", hidden).Invoke(ni, new object[] { true }); - } - - public static Bitmap AddBitmapOverlay(Bitmap original, params Bitmap[] overlays) - { - Bitmap bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format64bppArgb); - Graphics canvas = Graphics.FromImage(bitmap); - canvas.DrawImage(original, new Point(0, 0)); - foreach (Bitmap overlay in overlays) - { - canvas.DrawImage(new Bitmap(overlay, original.Size), new Point(0, 0)); - } - canvas.Save(); - return bitmap; - } - - public static Bitmap ChangeBitmapColor(Bitmap original, Color colorMask) - { - Bitmap newBitmap = new Bitmap(original); - - for (int x = 0; x < newBitmap.Width; x++) - { - for (int y = 0; y < newBitmap.Height; y++) - { - Color color = original.GetPixel(x, y); - if (color.A != 0) - { - int red = color.R * colorMask.R / 255; - int green = color.G * colorMask.G / 255; - int blue = color.B * colorMask.B / 255; - int alpha = color.A * colorMask.A / 255; - newBitmap.SetPixel(x, y, Color.FromArgb(alpha, red, green, blue)); - } - else - { - newBitmap.SetPixel(x, y, color); - } - } - } - return newBitmap; - } - - public static Bitmap ResizeBitmap(Bitmap original, int width, int height) - { - Bitmap newBitmap = new Bitmap(width, height); - using (Graphics g = Graphics.FromImage(newBitmap)) - { - g.SmoothingMode = SmoothingMode.HighQuality; - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - g.DrawImage(original, new Rectangle(0, 0, width, height)); - } - return newBitmap; - } - - public static int GetScreenDpi() - { - Graphics graphics = Graphics.FromHwnd(IntPtr.Zero); - int dpi = (int)graphics.DpiX; - graphics.Dispose(); - return dpi; - } - } -} diff --git a/shadowsocks-csharp/View/ConfigForm.Designer.cs b/shadowsocks-csharp/View/ConfigForm.Designer.cs deleted file mode 100755 index 3d5794a71..000000000 --- a/shadowsocks-csharp/View/ConfigForm.Designer.cs +++ /dev/null @@ -1,652 +0,0 @@ -namespace Shadowsocks.View -{ - partial class ConfigForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.PluginOptionsLabel = new System.Windows.Forms.Label(); - this.PluginTextBox = new System.Windows.Forms.TextBox(); - this.RemarksTextBox = new System.Windows.Forms.TextBox(); - this.IPLabel = new System.Windows.Forms.Label(); - this.ServerPortLabel = new System.Windows.Forms.Label(); - this.PasswordLabel = new System.Windows.Forms.Label(); - this.IPTextBox = new System.Windows.Forms.TextBox(); - this.ServerPortTextBox = new System.Windows.Forms.TextBox(); - this.PasswordTextBox = new System.Windows.Forms.TextBox(); - this.EncryptionLabel = new System.Windows.Forms.Label(); - this.EncryptionSelect = new System.Windows.Forms.ComboBox(); - this.TimeoutLabel = new System.Windows.Forms.Label(); - this.TimeoutTextBox = new System.Windows.Forms.TextBox(); - this.PluginLabel = new System.Windows.Forms.Label(); - this.PluginOptionsTextBox = new System.Windows.Forms.TextBox(); - this.ShowPasswdCheckBox = new System.Windows.Forms.CheckBox(); - this.PluginArgumentsTextBox = new System.Windows.Forms.TextBox(); - this.PluginArgumentsLabel = new System.Windows.Forms.Label(); - this.RemarksLabel = new System.Windows.Forms.Label(); - this.NeedPluginArgCheckBox = new System.Windows.Forms.CheckBox(); - this.panel2 = new System.Windows.Forms.Panel(); - this.OKButton = new System.Windows.Forms.Button(); - this.MyCancelButton = new System.Windows.Forms.Button(); - this.ApplyButton = new System.Windows.Forms.Button(); - this.DeleteButton = new System.Windows.Forms.Button(); - this.AddButton = new System.Windows.Forms.Button(); - this.ServerGroupBox = new System.Windows.Forms.GroupBox(); - this.ServersListBox = new System.Windows.Forms.ListBox(); - this.MoveDownButton = new System.Windows.Forms.Button(); - this.MoveUpButton = new System.Windows.Forms.Button(); - this.ProxyPortTextBox = new System.Windows.Forms.TextBox(); - this.ProxyPortLabel = new System.Windows.Forms.Label(); - this.PortableModeCheckBox = new System.Windows.Forms.CheckBox(); - this.DuplicateButton = new System.Windows.Forms.Button(); - this.toolTip1 = new System.Windows.Forms.ToolTip(this.components); - this.tableLayoutPanel7 = new System.Windows.Forms.TableLayoutPanel(); - this.GroupLabel = new System.Windows.Forms.Label(); - this.GroupTextBox = new System.Windows.Forms.TextBox(); - this.tableLayoutPanel1.SuspendLayout(); - this.ServerGroupBox.SuspendLayout(); - this.tableLayoutPanel7.SuspendLayout(); - this.SuspendLayout(); - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.AutoSize = true; - this.tableLayoutPanel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.tableLayoutPanel1.ColumnCount = 2; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.PluginOptionsLabel, 0, 6); - this.tableLayoutPanel1.Controls.Add(this.PluginTextBox, 1, 5); - this.tableLayoutPanel1.Controls.Add(this.RemarksTextBox, 1, 10); - this.tableLayoutPanel1.Controls.Add(this.IPLabel, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.ServerPortLabel, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.PasswordLabel, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.IPTextBox, 1, 0); - this.tableLayoutPanel1.Controls.Add(this.ServerPortTextBox, 1, 1); - this.tableLayoutPanel1.Controls.Add(this.PasswordTextBox, 1, 2); - this.tableLayoutPanel1.Controls.Add(this.EncryptionLabel, 0, 4); - this.tableLayoutPanel1.Controls.Add(this.EncryptionSelect, 1, 4); - this.tableLayoutPanel1.Controls.Add(this.TimeoutLabel, 0, 11); - this.tableLayoutPanel1.Controls.Add(this.TimeoutTextBox, 1, 11); - this.tableLayoutPanel1.Controls.Add(this.PluginLabel, 0, 5); - this.tableLayoutPanel1.Controls.Add(this.PluginOptionsTextBox, 1, 6); - this.tableLayoutPanel1.Controls.Add(this.ShowPasswdCheckBox, 1, 3); - this.tableLayoutPanel1.Controls.Add(this.PluginArgumentsTextBox, 1, 8); - this.tableLayoutPanel1.Controls.Add(this.PluginArgumentsLabel, 0, 8); - this.tableLayoutPanel1.Controls.Add(this.NeedPluginArgCheckBox, 1, 7); - this.tableLayoutPanel1.Controls.Add(this.GroupTextBox, 1, 12); - this.tableLayoutPanel1.Controls.Add(this.GroupLabel, 0, 12); - this.tableLayoutPanel1.Controls.Add(this.RemarksLabel, 0, 10); - this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel1.Location = new System.Drawing.Point(4, 22); - this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(0); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(4); - this.tableLayoutPanel1.RowCount = 14; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(362, 399); - this.tableLayoutPanel1.TabIndex = 0; - // - // PluginOptionsLabel - // - this.PluginOptionsLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.PluginOptionsLabel.AutoSize = true; - this.PluginOptionsLabel.Location = new System.Drawing.Point(24, 199); - this.PluginOptionsLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.PluginOptionsLabel.Name = "PluginOptionsLabel"; - this.PluginOptionsLabel.Size = new System.Drawing.Size(119, 15); - this.PluginOptionsLabel.TabIndex = 6; - this.PluginOptionsLabel.Text = "Plugin Options"; - this.toolTip1.SetToolTip(this.PluginOptionsLabel, "Environment variables for plugin program"); - // - // PluginTextBox - // - this.PluginTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.PluginTextBox.Location = new System.Drawing.Point(150, 163); - this.PluginTextBox.MaxLength = 256; - this.PluginTextBox.Name = "PluginTextBox"; - this.PluginTextBox.Size = new System.Drawing.Size(205, 25); - this.PluginTextBox.TabIndex = 5; - this.PluginTextBox.WordWrap = false; - // - // RemarksTextBox - // - this.RemarksTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.RemarksTextBox.Location = new System.Drawing.Point(150, 283); - this.RemarksTextBox.MaxLength = 32; - this.RemarksTextBox.Name = "RemarksTextBox"; - this.RemarksTextBox.Size = new System.Drawing.Size(205, 25); - this.RemarksTextBox.TabIndex = 8; - this.RemarksTextBox.WordWrap = false; - // - // IPLabel - // - this.IPLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.IPLabel.AutoSize = true; - this.IPLabel.Location = new System.Drawing.Point(64, 12); - this.IPLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.IPLabel.Name = "IPLabel"; - this.IPLabel.Size = new System.Drawing.Size(79, 15); - this.IPLabel.TabIndex = 0; - this.IPLabel.Text = "Server IP"; - // - // ServerPortLabel - // - this.ServerPortLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.ServerPortLabel.AutoSize = true; - this.ServerPortLabel.Location = new System.Drawing.Point(48, 43); - this.ServerPortLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.ServerPortLabel.Name = "ServerPortLabel"; - this.ServerPortLabel.Size = new System.Drawing.Size(95, 15); - this.ServerPortLabel.TabIndex = 1; - this.ServerPortLabel.Text = "Server Port"; - // - // PasswordLabel - // - this.PasswordLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.PasswordLabel.AutoSize = true; - this.PasswordLabel.Location = new System.Drawing.Point(72, 74); - this.PasswordLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.PasswordLabel.Name = "PasswordLabel"; - this.PasswordLabel.Size = new System.Drawing.Size(71, 15); - this.PasswordLabel.TabIndex = 2; - this.PasswordLabel.Text = "Password"; - this.PasswordLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - // - // IPTextBox - // - this.IPTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.IPTextBox.Location = new System.Drawing.Point(150, 7); - this.IPTextBox.MaxLength = 512; - this.IPTextBox.Name = "IPTextBox"; - this.IPTextBox.Size = new System.Drawing.Size(205, 25); - this.IPTextBox.TabIndex = 0; - this.IPTextBox.WordWrap = false; - // - // ServerPortTextBox - // - this.ServerPortTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.ServerPortTextBox.Location = new System.Drawing.Point(150, 38); - this.ServerPortTextBox.MaxLength = 10; - this.ServerPortTextBox.Name = "ServerPortTextBox"; - this.ServerPortTextBox.Size = new System.Drawing.Size(205, 25); - this.ServerPortTextBox.TabIndex = 1; - this.ServerPortTextBox.WordWrap = false; - // - // PasswordTextBox - // - this.PasswordTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.PasswordTextBox.Font = new System.Drawing.Font("Consolas", 9F); - this.PasswordTextBox.Location = new System.Drawing.Point(150, 69); - this.PasswordTextBox.MaxLength = 256; - this.PasswordTextBox.Name = "PasswordTextBox"; - this.PasswordTextBox.Size = new System.Drawing.Size(205, 25); - this.PasswordTextBox.TabIndex = 2; - this.PasswordTextBox.UseSystemPasswordChar = true; - this.PasswordTextBox.WordWrap = false; - // - // EncryptionLabel - // - this.EncryptionLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.EncryptionLabel.AutoSize = true; - this.EncryptionLabel.Location = new System.Drawing.Point(56, 134); - this.EncryptionLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.EncryptionLabel.Name = "EncryptionLabel"; - this.EncryptionLabel.Size = new System.Drawing.Size(87, 15); - this.EncryptionLabel.TabIndex = 4; - this.EncryptionLabel.Text = "Encryption"; - // - // EncryptionSelect - // - this.EncryptionSelect.Dock = System.Windows.Forms.DockStyle.Fill; - this.EncryptionSelect.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.EncryptionSelect.FormattingEnabled = true; - this.EncryptionSelect.ImeMode = System.Windows.Forms.ImeMode.NoControl; - this.EncryptionSelect.ItemHeight = 15; - this.EncryptionSelect.Location = new System.Drawing.Point(150, 129); - this.EncryptionSelect.Margin = new System.Windows.Forms.Padding(3, 5, 3, 8); - this.EncryptionSelect.Name = "EncryptionSelect"; - this.EncryptionSelect.Size = new System.Drawing.Size(205, 23); - this.EncryptionSelect.TabIndex = 4; - // - // TimeoutLabel - // - this.TimeoutLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.TimeoutLabel.AutoSize = true; - this.TimeoutLabel.Location = new System.Drawing.Point(40, 319); - this.TimeoutLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.TimeoutLabel.Name = "TimeoutLabel"; - this.TimeoutLabel.RightToLeft = System.Windows.Forms.RightToLeft.No; - this.TimeoutLabel.Size = new System.Drawing.Size(103, 15); - this.TimeoutLabel.TabIndex = 9; - this.TimeoutLabel.Text = "Timeout(Sec)"; - // - // TimeoutTextBox - // - this.TimeoutTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.TimeoutTextBox.Location = new System.Drawing.Point(150, 314); - this.TimeoutTextBox.MaxLength = 5; - this.TimeoutTextBox.Name = "TimeoutTextBox"; - this.TimeoutTextBox.Size = new System.Drawing.Size(205, 25); - this.TimeoutTextBox.TabIndex = 9; - // - // PluginLabel - // - this.PluginLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.PluginLabel.AutoSize = true; - this.PluginLabel.Location = new System.Drawing.Point(24, 168); - this.PluginLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.PluginLabel.Name = "PluginLabel"; - this.PluginLabel.Size = new System.Drawing.Size(119, 15); - this.PluginLabel.TabIndex = 5; - this.PluginLabel.Text = "Plugin Program"; - // - // PluginOptionsTextBox - // - this.PluginOptionsTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.PluginOptionsTextBox.Location = new System.Drawing.Point(150, 194); - this.PluginOptionsTextBox.MaxLength = 256; - this.PluginOptionsTextBox.Name = "PluginOptionsTextBox"; - this.PluginOptionsTextBox.Size = new System.Drawing.Size(205, 25); - this.PluginOptionsTextBox.TabIndex = 6; - this.PluginOptionsTextBox.WordWrap = false; - // - // ShowPasswdCheckBox - // - this.ShowPasswdCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left))); - this.ShowPasswdCheckBox.AutoSize = true; - this.ShowPasswdCheckBox.Location = new System.Drawing.Point(151, 101); - this.ShowPasswdCheckBox.Margin = new System.Windows.Forms.Padding(4); - this.ShowPasswdCheckBox.Name = "ShowPasswdCheckBox"; - this.ShowPasswdCheckBox.Size = new System.Drawing.Size(133, 19); - this.ShowPasswdCheckBox.TabIndex = 3; - this.ShowPasswdCheckBox.Text = "Show Password"; - this.ShowPasswdCheckBox.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.ShowPasswdCheckBox.UseVisualStyleBackColor = true; - this.ShowPasswdCheckBox.CheckedChanged += new System.EventHandler(this.ShowPasswdCheckBox_CheckedChanged); - // - // PluginArgumentsTextBox - // - this.PluginArgumentsTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.PluginArgumentsTextBox.Location = new System.Drawing.Point(150, 252); - this.PluginArgumentsTextBox.MaxLength = 512; - this.PluginArgumentsTextBox.Name = "PluginArgumentsTextBox"; - this.PluginArgumentsTextBox.Size = new System.Drawing.Size(205, 25); - this.PluginArgumentsTextBox.TabIndex = 7; - this.PluginArgumentsTextBox.WordWrap = false; - // - // PluginArgumentsLabel - // - this.PluginArgumentsLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.PluginArgumentsLabel.AutoSize = true; - this.PluginArgumentsLabel.Location = new System.Drawing.Point(8, 257); - this.PluginArgumentsLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.PluginArgumentsLabel.Name = "PluginArgumentsLabel"; - this.PluginArgumentsLabel.Size = new System.Drawing.Size(135, 15); - this.PluginArgumentsLabel.TabIndex = 7; - this.PluginArgumentsLabel.Text = "Plugin Arguments"; - this.toolTip1.SetToolTip(this.PluginArgumentsLabel, "Not a SIP003 standard. Used as CLI arguments.\r\nMandatory:\r\n%SS_LOCAL_HOST%, %SS_L" + - "OCAL_PORT%, %SS_REMOTE_HOST%, %SS_REMOTE_PORT%\r\nOptional:\r\n%SS_PLUGIN_OPTIONS%"); - // - // RemarksLabel - // - this.RemarksLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.RemarksLabel.AutoSize = true; - this.RemarksLabel.Location = new System.Drawing.Point(80, 288); - this.RemarksLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.RemarksLabel.Name = "RemarksLabel"; - this.RemarksLabel.Size = new System.Drawing.Size(63, 15); - this.RemarksLabel.TabIndex = 8; - this.RemarksLabel.Text = "Remarks"; - // - // NeedPluginArgCheckBox - // - this.NeedPluginArgCheckBox.AutoSize = true; - this.NeedPluginArgCheckBox.Location = new System.Drawing.Point(151, 226); - this.NeedPluginArgCheckBox.Margin = new System.Windows.Forms.Padding(4); - this.NeedPluginArgCheckBox.Name = "NeedPluginArgCheckBox"; - this.NeedPluginArgCheckBox.Size = new System.Drawing.Size(189, 19); - this.NeedPluginArgCheckBox.TabIndex = 10; - this.NeedPluginArgCheckBox.Text = "Need Plugin Argument"; - this.NeedPluginArgCheckBox.UseVisualStyleBackColor = true; - this.NeedPluginArgCheckBox.CheckedChanged += new System.EventHandler(this.UsePluginArgCheckBox_CheckedChanged); - // - // panel2 - // - this.panel2.Anchor = System.Windows.Forms.AnchorStyles.Top; - this.panel2.AutoSize = true; - this.panel2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.panel2.Location = new System.Drawing.Point(190, 229); - this.panel2.Margin = new System.Windows.Forms.Padding(4); - this.panel2.Name = "panel2"; - this.panel2.Size = new System.Drawing.Size(0, 0); - this.panel2.TabIndex = 1; - // - // OKButton - // - this.OKButton.DialogResult = System.Windows.Forms.DialogResult.OK; - this.OKButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.OKButton.Location = new System.Drawing.Point(282, 501); - this.OKButton.Name = "OKButton"; - this.OKButton.Size = new System.Drawing.Size(87, 29); - this.OKButton.TabIndex = 17; - this.OKButton.Text = "OK"; - this.OKButton.UseVisualStyleBackColor = true; - this.OKButton.Click += new System.EventHandler(this.OKButton_Click); - // - // MyCancelButton - // - this.MyCancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.MyCancelButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.MyCancelButton.Location = new System.Drawing.Point(375, 501); - this.MyCancelButton.Name = "MyCancelButton"; - this.MyCancelButton.Size = new System.Drawing.Size(87, 29); - this.MyCancelButton.TabIndex = 18; - this.MyCancelButton.Text = "Cancel"; - this.MyCancelButton.UseVisualStyleBackColor = true; - this.MyCancelButton.Click += new System.EventHandler(this.CancelButton_Click); - // - // ApplyButton - // - this.ApplyButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.ApplyButton.Enabled = false; - this.ApplyButton.Location = new System.Drawing.Point(468, 501); - this.ApplyButton.Name = "ApplyButton"; - this.ApplyButton.Size = new System.Drawing.Size(91, 29); - this.ApplyButton.TabIndex = 19; - this.ApplyButton.Text = "Apply"; - this.ApplyButton.UseVisualStyleBackColor = true; - this.ApplyButton.Click += new System.EventHandler(this.ApplyButton_Click); - // - // DeleteButton - // - this.DeleteButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.DeleteButton.Location = new System.Drawing.Point(96, 431); - this.DeleteButton.Name = "DeleteButton"; - this.DeleteButton.Size = new System.Drawing.Size(87, 29); - this.DeleteButton.TabIndex = 13; - this.DeleteButton.Text = "&Delete"; - this.DeleteButton.UseVisualStyleBackColor = true; - this.DeleteButton.Click += new System.EventHandler(this.DeleteButton_Click); - // - // AddButton - // - this.AddButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.AddButton.Location = new System.Drawing.Point(3, 431); - this.AddButton.Name = "AddButton"; - this.AddButton.Size = new System.Drawing.Size(87, 29); - this.AddButton.TabIndex = 12; - this.AddButton.Text = "&Add"; - this.AddButton.UseVisualStyleBackColor = true; - this.AddButton.Click += new System.EventHandler(this.AddButton_Click); - // - // ServerGroupBox - // - this.ServerGroupBox.AutoSize = true; - this.ServerGroupBox.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.tableLayoutPanel7.SetColumnSpan(this.ServerGroupBox, 4); - this.ServerGroupBox.Controls.Add(this.tableLayoutPanel1); - this.ServerGroupBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.ServerGroupBox.Location = new System.Drawing.Point(189, 0); - this.ServerGroupBox.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3); - this.ServerGroupBox.Name = "ServerGroupBox"; - this.ServerGroupBox.Padding = new System.Windows.Forms.Padding(4); - this.ServerGroupBox.Size = new System.Drawing.Size(370, 425); - this.ServerGroupBox.TabIndex = 0; - this.ServerGroupBox.TabStop = false; - this.ServerGroupBox.Text = "Server"; - // - // ServersListBox - // - this.tableLayoutPanel7.SetColumnSpan(this.ServersListBox, 2); - this.ServersListBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.ServersListBox.FormattingEnabled = true; - this.ServersListBox.IntegralHeight = false; - this.ServersListBox.ItemHeight = 15; - this.ServersListBox.Location = new System.Drawing.Point(3, 3); - this.ServersListBox.Name = "ServersListBox"; - this.ServersListBox.Size = new System.Drawing.Size(180, 422); - this.ServersListBox.TabIndex = 11; - this.ServersListBox.SelectedIndexChanged += new System.EventHandler(this.ServersListBox_SelectedIndexChanged); - // - // MoveDownButton - // - this.MoveDownButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.MoveDownButton.Location = new System.Drawing.Point(96, 501); - this.MoveDownButton.Name = "MoveDownButton"; - this.MoveDownButton.Size = new System.Drawing.Size(87, 29); - this.MoveDownButton.TabIndex = 16; - this.MoveDownButton.Text = "Move D&own"; - this.MoveDownButton.UseVisualStyleBackColor = true; - this.MoveDownButton.Click += new System.EventHandler(this.MoveDownButton_Click); - // - // MoveUpButton - // - this.MoveUpButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.MoveUpButton.Location = new System.Drawing.Point(3, 501); - this.MoveUpButton.Name = "MoveUpButton"; - this.MoveUpButton.Size = new System.Drawing.Size(87, 29); - this.MoveUpButton.TabIndex = 15; - this.MoveUpButton.Text = "Move &Up"; - this.MoveUpButton.UseVisualStyleBackColor = true; - this.MoveUpButton.Click += new System.EventHandler(this.MoveUpButton_Click); - // - // ProxyPortTextBox - // - this.tableLayoutPanel7.SetColumnSpan(this.ProxyPortTextBox, 2); - this.ProxyPortTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.ProxyPortTextBox.Location = new System.Drawing.Point(283, 432); - this.ProxyPortTextBox.Margin = new System.Windows.Forms.Padding(4); - this.ProxyPortTextBox.MaxLength = 10; - this.ProxyPortTextBox.Name = "ProxyPortTextBox"; - this.ProxyPortTextBox.Size = new System.Drawing.Size(178, 25); - this.ProxyPortTextBox.TabIndex = 10; - this.ProxyPortTextBox.WordWrap = false; - // - // ProxyPortLabel - // - this.ProxyPortLabel.AutoSize = true; - this.ProxyPortLabel.Dock = System.Windows.Forms.DockStyle.Fill; - this.ProxyPortLabel.Location = new System.Drawing.Point(190, 428); - this.ProxyPortLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.ProxyPortLabel.Name = "ProxyPortLabel"; - this.ProxyPortLabel.Size = new System.Drawing.Size(85, 35); - this.ProxyPortLabel.TabIndex = 10; - this.ProxyPortLabel.Text = "Proxy Port"; - this.ProxyPortLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight; - // - // PortableModeCheckBox - // - this.PortableModeCheckBox.AutoSize = true; - this.tableLayoutPanel7.SetColumnSpan(this.PortableModeCheckBox, 2); - this.PortableModeCheckBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.PortableModeCheckBox.Location = new System.Drawing.Point(283, 467); - this.PortableModeCheckBox.Margin = new System.Windows.Forms.Padding(4); - this.PortableModeCheckBox.Name = "PortableModeCheckBox"; - this.PortableModeCheckBox.Size = new System.Drawing.Size(178, 27); - this.PortableModeCheckBox.TabIndex = 11; - this.PortableModeCheckBox.Text = "Portable Mode"; - this.toolTip1.SetToolTip(this.PortableModeCheckBox, "restart required"); - this.PortableModeCheckBox.UseVisualStyleBackColor = true; - // - // DuplicateButton - // - this.DuplicateButton.Dock = System.Windows.Forms.DockStyle.Fill; - this.DuplicateButton.Location = new System.Drawing.Point(3, 466); - this.DuplicateButton.Name = "DuplicateButton"; - this.DuplicateButton.Size = new System.Drawing.Size(87, 29); - this.DuplicateButton.TabIndex = 14; - this.DuplicateButton.Text = "Dupli&cate"; - this.DuplicateButton.UseVisualStyleBackColor = true; - this.DuplicateButton.Click += new System.EventHandler(this.DuplicateButton_Click); - // - // tableLayoutPanel7 - // - this.tableLayoutPanel7.AutoSize = true; - this.tableLayoutPanel7.ColumnCount = 6; - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 16.66667F)); - this.tableLayoutPanel7.Controls.Add(this.ApplyButton, 5, 3); - this.tableLayoutPanel7.Controls.Add(this.MyCancelButton, 4, 3); - this.tableLayoutPanel7.Controls.Add(this.OKButton, 3, 3); - this.tableLayoutPanel7.Controls.Add(this.ProxyPortTextBox, 3, 1); - this.tableLayoutPanel7.Controls.Add(this.MoveDownButton, 1, 3); - this.tableLayoutPanel7.Controls.Add(this.ProxyPortLabel, 2, 1); - this.tableLayoutPanel7.Controls.Add(this.MoveUpButton, 0, 3); - this.tableLayoutPanel7.Controls.Add(this.AddButton, 0, 1); - this.tableLayoutPanel7.Controls.Add(this.DeleteButton, 1, 1); - this.tableLayoutPanel7.Controls.Add(this.DuplicateButton, 0, 2); - this.tableLayoutPanel7.Controls.Add(this.ServersListBox, 0, 0); - this.tableLayoutPanel7.Controls.Add(this.ServerGroupBox, 2, 0); - this.tableLayoutPanel7.Controls.Add(this.PortableModeCheckBox, 3, 2); - this.tableLayoutPanel7.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel7.GrowStyle = System.Windows.Forms.TableLayoutPanelGrowStyle.FixedSize; - this.tableLayoutPanel7.Location = new System.Drawing.Point(10, 10); - this.tableLayoutPanel7.Name = "tableLayoutPanel7"; - this.tableLayoutPanel7.RowCount = 4; - this.tableLayoutPanel7.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel7.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel7.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel7.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel7.Size = new System.Drawing.Size(562, 533); - this.tableLayoutPanel7.TabIndex = 8; - // - // GroupLabel - // - this.GroupLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.GroupLabel.AutoSize = true; - this.GroupLabel.Location = new System.Drawing.Point(96, 350); - this.GroupLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.GroupLabel.Name = "GroupLabel"; - this.GroupLabel.RightToLeft = System.Windows.Forms.RightToLeft.No; - this.GroupLabel.Size = new System.Drawing.Size(47, 15); - this.GroupLabel.TabIndex = 11; - this.GroupLabel.Text = "Group"; - // - // GroupTextBox - // - this.GroupTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.GroupTextBox.Location = new System.Drawing.Point(150, 345); - this.GroupTextBox.MaxLength = 5; - this.GroupTextBox.Name = "GroupTextBox"; - this.GroupTextBox.ReadOnly = true; - this.GroupTextBox.Size = new System.Drawing.Size(205, 25); - this.GroupTextBox.TabIndex = 12; - // - // ConfigForm - // - this.AcceptButton = this.OKButton; - this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.CancelButton = this.MyCancelButton; - this.ClientSize = new System.Drawing.Size(582, 553); - this.Controls.Add(this.tableLayoutPanel7); - this.Controls.Add(this.panel2); - this.Margin = new System.Windows.Forms.Padding(4); - this.MinimizeBox = false; - this.MinimumSize = new System.Drawing.Size(400, 575); - this.Name = "ConfigForm"; - this.Padding = new System.Windows.Forms.Padding(10); - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "Edit Servers"; - this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.ConfigForm_FormClosed); - this.Shown += new System.EventHandler(this.ConfigForm_Shown); - this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ConfigForm_KeyDown); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.ServerGroupBox.ResumeLayout(false); - this.ServerGroupBox.PerformLayout(); - this.tableLayoutPanel7.ResumeLayout(false); - this.tableLayoutPanel7.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.Label IPLabel; - private System.Windows.Forms.Label ServerPortLabel; - private System.Windows.Forms.Label PasswordLabel; - private System.Windows.Forms.TextBox IPTextBox; - private System.Windows.Forms.TextBox ServerPortTextBox; - private System.Windows.Forms.Label EncryptionLabel; - private System.Windows.Forms.ComboBox EncryptionSelect; - private System.Windows.Forms.Panel panel2; - private System.Windows.Forms.Button OKButton; - private System.Windows.Forms.Button MyCancelButton; - private System.Windows.Forms.Button ApplyButton; - private System.Windows.Forms.Button DeleteButton; - private System.Windows.Forms.Button AddButton; - private System.Windows.Forms.GroupBox ServerGroupBox; - private System.Windows.Forms.ListBox ServersListBox; - private System.Windows.Forms.TextBox RemarksTextBox; - private System.Windows.Forms.Label RemarksLabel; - private System.Windows.Forms.TextBox ProxyPortTextBox; - private System.Windows.Forms.Label ProxyPortLabel; - private System.Windows.Forms.Button MoveDownButton; - private System.Windows.Forms.Button MoveUpButton; - private System.Windows.Forms.Button DuplicateButton; - private System.Windows.Forms.Label TimeoutLabel; - private System.Windows.Forms.TextBox TimeoutTextBox; - private System.Windows.Forms.Label PluginOptionsLabel; - private System.Windows.Forms.TextBox PluginTextBox; - private System.Windows.Forms.Label PluginLabel; - private System.Windows.Forms.TextBox PluginOptionsTextBox; - private System.Windows.Forms.CheckBox ShowPasswdCheckBox; - private System.Windows.Forms.TextBox PasswordTextBox; - private System.Windows.Forms.TextBox PluginArgumentsTextBox; - private System.Windows.Forms.Label PluginArgumentsLabel; - private System.Windows.Forms.ToolTip toolTip1; - private System.Windows.Forms.CheckBox PortableModeCheckBox; - private System.Windows.Forms.CheckBox NeedPluginArgCheckBox; - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel7; - private System.Windows.Forms.Label GroupLabel; - private System.Windows.Forms.TextBox GroupTextBox; - } -} - diff --git a/shadowsocks-csharp/View/ConfigForm.cs b/shadowsocks-csharp/View/ConfigForm.cs deleted file mode 100755 index a60788ee4..000000000 --- a/shadowsocks-csharp/View/ConfigForm.cs +++ /dev/null @@ -1,622 +0,0 @@ -using Shadowsocks.Controller; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace Shadowsocks.View -{ - public partial class ConfigForm : Form - { - private ShadowsocksController controller; - - // this is a copy of configuration that we are working on - private Configuration _modifiedConfiguration; - private int _lastSelectedIndex = -1; - - private bool isChange = false; - - private class EncryptionMethod - { - public readonly string name; - public readonly bool deprecated; - - // Edit here to add/delete encryption method displayed in UI - private static string[] inuseMethod = new string[] - { - "none", - "plain", - "aes-256-gcm", - "aes-192-gcm", - "aes-128-gcm", - "chacha20-ietf-poly1305", - "xchacha20-ietf-poly1305", - }; - public static EncryptionMethod[] AllMethods - { - get - { - if (!init) Init(); - return allMethods; - } - } - private static bool init = false; - private static EncryptionMethod[] allMethods; - private static Dictionary methodByName = new Dictionary(); - private static void Init() - { - var all = new List(); - - all.AddRange(inuseMethod.Select(i => new EncryptionMethod(i, false))); - - allMethods = all.ToArray(); - foreach (var item in all) - { - methodByName[item.name] = item; - } - init = true; - } - - public static EncryptionMethod GetMethod(string name) - { - if (!init) Init(); - bool success = methodByName.TryGetValue(name, out EncryptionMethod method); - if (!success) - { - string defaultMethod = Server.DefaultMethod; - MessageBox.Show(I18N.GetString("Encryption method {0} not exist, will replace with {1}", name, defaultMethod), I18N.GetString("Shadowsocks")); - return methodByName[defaultMethod]; - } - return method; - } - - private EncryptionMethod(string name, bool deprecated) - { - this.name = name; - this.deprecated = deprecated; - } - - public override string ToString() - { - return deprecated ? $"{name} ({I18N.GetString("deprecated")})" : name; - } - } - - public ConfigForm(ShadowsocksController controller) - { - Font = SystemFonts.MessageBoxFont; - InitializeComponent(); - EncryptionSelect.Items.AddRange(EncryptionMethod.AllMethods); - - PerformLayout(); - - UpdateTexts(); - SetupValueChangedListeners(); - Icon = Icon.FromHandle(Resources.ssw128.GetHicon()); - - this.controller = controller; - controller.ConfigChanged += Controller_ConfigChanged; - - LoadCurrentConfiguration(); - } - - private void UpdateTexts() - { - I18N.TranslateForm(this); - toolTip1.SetToolTip(PortableModeCheckBox, I18N.GetString("Restart required")); - } - - private void SetupValueChangedListeners() - { - IPTextBox.TextChanged += ConfigValueChanged; - ProxyPortTextBox.TextChanged += ConfigValueChanged; - PasswordTextBox.TextChanged += ConfigValueChanged; - EncryptionSelect.SelectedIndexChanged += ConfigValueChanged; - PluginTextBox.TextChanged += ConfigValueChanged; - PluginArgumentsTextBox.TextChanged += ConfigValueChanged; - PluginOptionsTextBox.TextChanged += ConfigValueChanged; - RemarksTextBox.TextChanged += ConfigValueChanged; - TimeoutTextBox.TextChanged += ConfigValueChanged; - PortableModeCheckBox.CheckedChanged += ConfigValueChanged; - ServerPortTextBox.TextChanged += ConfigValueChanged; - } - - private void Controller_ConfigChanged(object sender, EventArgs e) - { - LoadCurrentConfiguration(); - } - - private void ConfigValueChanged(object sender, EventArgs e) - { - isChange = true; - ApplyButton.Enabled = true; - } - - private bool ValidateAndSaveSelectedServerDetails(bool isSave = false, bool isCopy = false) - { - try - { - if (_lastSelectedIndex == -1 || _lastSelectedIndex >= _modifiedConfiguration.configs.Count) - { - return true; - } - - bool verify = GetServerDetailsFromUI(out Server server, isSave, isCopy); - - if (server != null) - { - if (isSave || isCopy) - Configuration.CheckServer(server); - - _modifiedConfiguration.configs[_lastSelectedIndex] = server; - } - return verify; - } - catch (Exception ex) - { - MessageBox.Show(ex.Message); - } - return false; - } - - private bool GetServerDetailsFromUI(out Server server, bool isSave = false, bool isCopy = false) - { - server = null; - - bool? checkIP = false; - bool? checkPort = false; - bool? checkPassword = false; - bool? checkTimeout = false; - - if ((checkIP = CheckIPTextBox(out string address, isSave, isCopy)).GetValueOrDefault(false) && address != null - && (checkPort = CheckServerPortTextBox(out int? addressPort, isSave, isCopy)).GetValueOrDefault(false) && addressPort.HasValue - && (checkPassword = CheckPasswordTextBox(out string serverPassword, isSave, isCopy)).GetValueOrDefault(false) && serverPassword != null - && (checkTimeout = CheckTimeoutTextBox(out int? timeout, isSave, isCopy)).GetValueOrDefault(false) && timeout.HasValue) - { - server = new Server() - { - server = address, - server_port = addressPort.Value, - password = serverPassword, - method = ((EncryptionMethod)EncryptionSelect.SelectedItem).name, - plugin = PluginTextBox.Text, - plugin_opts = PluginOptionsTextBox.Text, - plugin_args = PluginArgumentsTextBox.Text, - remarks = RemarksTextBox.Text, - timeout = timeout.Value, - group = GroupTextBox.Text - }; - - return true; - } - - if (checkIP == null || checkPort == null || checkTimeout == null) - { - _modifiedConfiguration.configs.RemoveAt(_lastSelectedIndex); - ServersListBox.SelectedIndexChanged -= ServersListBox_SelectedIndexChanged; - - int lastIndex = ServersListBox.SelectedIndex; - - LoadServerNameListToUI(_modifiedConfiguration); - - _lastSelectedIndex = (ServersListBox.SelectedIndex = (_lastSelectedIndex == ServersListBox.Items.Count ? lastIndex : lastIndex - 1)); - - ServersListBox.SelectedIndexChanged += ServersListBox_SelectedIndexChanged; - return true; - } - else - return false; - } - - #region GetServerDetailsFromUI Check - - private bool? CheckIPTextBox(out string address, bool isSave, bool isCopy) - { - address = null; - string outAddress; - if (Uri.CheckHostName(outAddress = IPTextBox.Text.Trim()) == UriHostNameType.Unknown) - { - if (!isSave && !isCopy && ServersListBox.Items.Count > 1 && I18N.GetString("New server").Equals(ServersListBox.Items[_lastSelectedIndex].ToString())) - { - DialogResult result = MessageBox.Show(I18N.GetString("Whether to discard unconfigured servers"), I18N.GetString("Operation failure"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.OK) - return null; - } - else if (isChange && !isSave && !isCopy) - { - var result = MessageBox.Show(I18N.GetString("Invalid server address, Cannot automatically save or discard changes"), I18N.GetString("Auto save failed"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - return false; - else - { - address = _modifiedConfiguration.configs[_lastSelectedIndex].server; - return true; - } - } - else - { - MessageBox.Show(I18N.GetString("Invalid server address"), I18N.GetString("Operation failure")); - IPTextBox.Focus(); - } - return false; - } - else - { - address = outAddress; - } - return true; - } - - private bool? CheckServerPortTextBox(out int? addressPort, bool isSave, bool isCopy) - { - addressPort = null; - if (!int.TryParse(ServerPortTextBox.Text, out int outaddressPort)) - { - if (!isSave && !isCopy && ServersListBox.Items.Count > 1 && I18N.GetString("New server").Equals(ServersListBox.Items[_lastSelectedIndex].ToString())) - { - DialogResult result = MessageBox.Show(I18N.GetString("Whether to discard unconfigured servers"), I18N.GetString("Operation failure"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.OK) - return null; - } - else if (isChange && !isSave && !isCopy) - { - var result = MessageBox.Show(I18N.GetString("Illegal port number format, Cannot automatically save or discard changes"), I18N.GetString("Auto save failed"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - return false; - else - { - addressPort = _modifiedConfiguration.configs[_lastSelectedIndex].server_port; - return true; - } - } - else - { - MessageBox.Show(I18N.GetString("Illegal port number format"), I18N.GetString("Operation failure")); - ServerPortTextBox.Focus(); - } - return false; - } - else - { - addressPort = outaddressPort; - } - return true; - } - - private bool? CheckPasswordTextBox(out string password, bool isSave, bool isCopy) - { - password = null; - string outPassword; - if (string.IsNullOrWhiteSpace(outPassword = PasswordTextBox.Text)) - { - if (!isSave && !isCopy && ServersListBox.Items.Count > 1 && I18N.GetString("New server").Equals(ServersListBox.Items[_lastSelectedIndex].ToString())) - { - DialogResult result = MessageBox.Show(I18N.GetString("Whether to discard unconfigured servers"), I18N.GetString("Operation failure"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.OK) - return null; - } - else if (isChange && !isSave && !isCopy) - { - var result = MessageBox.Show(I18N.GetString("Password can not be blank, Cannot automatically save or discard changes"), I18N.GetString("Auto save failed"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - return false; - else - { - password = _modifiedConfiguration.configs[_lastSelectedIndex].password; - return true; - } - } - else - { - MessageBox.Show(I18N.GetString("Password can not be blank"), I18N.GetString("Operation failure")); - PasswordTextBox.Focus(); - } - return false; - } - else - { - password = outPassword; - } - return true; - } - - private bool? CheckTimeoutTextBox(out int? timeout, bool isSave, bool isCopy) - { - timeout = null; - if (!int.TryParse(TimeoutTextBox.Text, out int outTimeout)) - { - if (!isSave && !isCopy && ServersListBox.Items.Count > 1 && I18N.GetString("New server").Equals(ServersListBox.Items[_lastSelectedIndex].ToString())) - { - DialogResult result = MessageBox.Show(I18N.GetString("Whether to discard unconfigured servers"), I18N.GetString("Operation failure"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.OK) - return null; - } - else if (isChange && !isSave && !isCopy) - { - var result = MessageBox.Show(I18N.GetString("Illegal timeout format, Cannot automatically save or discard changes"), I18N.GetString("Auto save failed"), MessageBoxButtons.OKCancel); - - if (result == DialogResult.Cancel) - return false; - else - { - timeout = _modifiedConfiguration.configs[_lastSelectedIndex].timeout; - return true; - } - } - else - { - MessageBox.Show(I18N.GetString("Illegal timeout format"), I18N.GetString("Operation failure")); - TimeoutTextBox.Focus(); - } - return false; - } - else - { - timeout = outTimeout; - } - return true; - } - - #endregion - - private void LoadSelectedServerDetails() - { - if (ServersListBox.SelectedIndex >= 0 && ServersListBox.SelectedIndex < _modifiedConfiguration.configs.Count) - { - Server server = _modifiedConfiguration.configs[ServersListBox.SelectedIndex]; - SetServerDetailsToUI(server); - } - } - - private void SetServerDetailsToUI(Server server) - { - IPTextBox.Text = server.server; - ServerPortTextBox.Text = server.server_port.ToString(); - PasswordTextBox.Text = server.password; - EncryptionSelect.SelectedItem = EncryptionMethod.GetMethod(server.method ?? Server.DefaultMethod); - PluginTextBox.Text = server.plugin; - PluginOptionsTextBox.Text = server.plugin_opts; - PluginArgumentsTextBox.Text = server.plugin_args; - - bool showPluginArgInput = !string.IsNullOrEmpty(server.plugin_args); - NeedPluginArgCheckBox.Checked = showPluginArgInput; - ShowHidePluginArgInput(showPluginArgInput); - - RemarksTextBox.Text = server.remarks; - TimeoutTextBox.Text = server.timeout.ToString(); - - GroupTextBox.Text = server.group; - - isChange = false; - } - - private void ShowHidePluginArgInput(bool show) - { - PluginArgumentsTextBox.Visible = show; - PluginArgumentsLabel.Visible = show; - } - - private void LoadServerNameListToUI(Configuration configuration) - { - ServersListBox.Items.Clear(); - foreach (Server server in configuration.configs) - { - ServersListBox.Items.Add(server.ToString()); - } - } - - private void LoadCurrentConfiguration() - { - _modifiedConfiguration = controller.GetCurrentConfiguration(); - LoadServerNameListToUI(_modifiedConfiguration); - - _lastSelectedIndex = _modifiedConfiguration.index; - if (_lastSelectedIndex < 0 || _lastSelectedIndex >= ServersListBox.Items.Count) - { - _lastSelectedIndex = 0; - } - - ServersListBox.SelectedIndex = _lastSelectedIndex; - UpdateButtons(); - LoadSelectedServerDetails(); - - ProxyPortTextBox.Text = _modifiedConfiguration.localPort.ToString(); - PortableModeCheckBox.Checked = _modifiedConfiguration.portableMode; - - ApplyButton.Enabled = false; - } - - private bool SaveValidConfiguration() - { - if (!ValidateAndSaveSelectedServerDetails(isSave: true)) - { - return false; - } - - int localPort = int.Parse(ProxyPortTextBox.Text); - Configuration.CheckLocalPort(localPort); - _modifiedConfiguration.localPort = localPort; - - _modifiedConfiguration.portableMode = PortableModeCheckBox.Checked; - - controller.SaveServers(_modifiedConfiguration.configs, _modifiedConfiguration.localPort, _modifiedConfiguration.portableMode); - // SelectedIndex remains valid - // We handled this in event handlers, e.g. Add/DeleteButton, SelectedIndexChanged - // and move operations - controller.SelectServerIndex(ServersListBox.SelectedIndex); - return true; - } - - private void ConfigForm_KeyDown(object sender, KeyEventArgs e) - { - // Sometimes the users may hit enter key by mistake, and the form will close without saving entries. - - if (e.KeyCode == Keys.Enter) - { - SaveValidConfiguration(); - } - } - - private void ServersListBox_SelectedIndexChanged(object sender, EventArgs e) - { - if (!ServersListBox.CanSelect) - { - return; - } - if (_lastSelectedIndex == ServersListBox.SelectedIndex) - { - // we are moving back to oldSelectedIndex or doing a force move - return; - } - if (!ValidateAndSaveSelectedServerDetails()) - { - // why this won't cause stack overflow? - ServersListBox.SelectedIndex = _lastSelectedIndex; - return; - } - if (_lastSelectedIndex >= 0 && _lastSelectedIndex < _modifiedConfiguration.configs.Count) - { - ServersListBox.Items[_lastSelectedIndex] = _modifiedConfiguration.configs[_lastSelectedIndex].ToString(); - } - UpdateButtons(); - LoadSelectedServerDetails(); - _lastSelectedIndex = ServersListBox.SelectedIndex; - } - - private void AddButton_Click(object sender, EventArgs e) - { - if (ValidateAndSaveSelectedServerDetails(isSave: true)) - { - Configuration.AddDefaultServerOrServer(_modifiedConfiguration); - LoadServerNameListToUI(_modifiedConfiguration); - _lastSelectedIndex = (ServersListBox.SelectedIndex = _modifiedConfiguration.configs.Count - 1); - } - } - - private void DuplicateButton_Click(object sender, EventArgs e) - { - if (ValidateAndSaveSelectedServerDetails(isCopy: true)) - { - Server currServer = _modifiedConfiguration.configs[_lastSelectedIndex]; - Configuration.AddDefaultServerOrServer(_modifiedConfiguration, currServer, _lastSelectedIndex + 1); - LoadServerNameListToUI(_modifiedConfiguration); - _lastSelectedIndex = (ServersListBox.SelectedIndex = (_lastSelectedIndex + 1)); - } - } - - private void DeleteButton_Click(object sender, EventArgs e) - { - _modifiedConfiguration.configs.RemoveAt(_lastSelectedIndex); - - if (_modifiedConfiguration.configs.Count == 0) - { - Configuration.AddDefaultServerOrServer(_modifiedConfiguration); - } - - LoadServerNameListToUI(_modifiedConfiguration); - ServersListBox.SelectedIndexChanged -= ServersListBox_SelectedIndexChanged; - - _lastSelectedIndex = (ServersListBox.SelectedIndex = (_lastSelectedIndex >= _modifiedConfiguration.configs.Count ? (_modifiedConfiguration.configs.Count - 1) : _lastSelectedIndex)); - - ServersListBox.SelectedIndexChanged += ServersListBox_SelectedIndexChanged; - LoadSelectedServerDetails(); - - UpdateButtons(); - } - - private void UpdateButtons() - { - DeleteButton.Enabled = (ServersListBox.Items.Count > 0); - MoveUpButton.Enabled = (ServersListBox.SelectedIndex > 0); - MoveDownButton.Enabled = (ServersListBox.SelectedIndex < ServersListBox.Items.Count - 1); - } - - private void MoveUpButton_Click(object sender, EventArgs e) - { - if (ServersListBox.SelectedIndex > 0) - { - MoveConfigItem(-1); // -1 means move backward - } - } - - private void MoveDownButton_Click(object sender, EventArgs e) - { - if (ServersListBox.SelectedIndex < ServersListBox.Items.Count - 1) - { - MoveConfigItem(+1); // +1 means move forward - } - } - - private void MoveConfigItem(int step) - { - var server = _modifiedConfiguration.configs[_lastSelectedIndex]; - var newIndex = _lastSelectedIndex + step; - - _modifiedConfiguration.configs.RemoveAt(_lastSelectedIndex); - _modifiedConfiguration.configs.Insert(newIndex, server); - - ServersListBox.BeginUpdate(); - - LoadServerNameListToUI(_modifiedConfiguration); - - _lastSelectedIndex = newIndex; - ServersListBox.SelectedIndex = newIndex; - ServersListBox.EndUpdate(); - - UpdateButtons(); - } - - private void OKButton_Click(object sender, EventArgs e) - { - if (SaveValidConfiguration()) - { - Close(); - } - } - - private void CancelButton_Click(object sender, EventArgs e) - { - Close(); - } - - private void ApplyButton_Click(object sender, EventArgs e) - { - SaveValidConfiguration(); - } - - private void ConfigForm_Shown(object sender, EventArgs e) - { - IPTextBox.Focus(); - } - - private void ConfigForm_FormClosed(object sender, FormClosedEventArgs e) - { - controller.ConfigChanged -= Controller_ConfigChanged; - } - - private void ShowPasswdCheckBox_CheckedChanged(object sender, EventArgs e) - { - PasswordTextBox.UseSystemPasswordChar = !ShowPasswdCheckBox.Checked; - } - - private void UsePluginArgCheckBox_CheckedChanged(object sender, EventArgs e) - { - ShowHidePluginArgInput(NeedPluginArgCheckBox.Checked); - } - - private void EncryptionSelect_SelectedIndexChanged(object sender, EventArgs e) - { - - } - } -} diff --git a/shadowsocks-csharp/View/ConfigForm.resx b/shadowsocks-csharp/View/ConfigForm.resx deleted file mode 100755 index 6dfd76d8d..000000000 --- a/shadowsocks-csharp/View/ConfigForm.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 17 - - - 17, 17 - - \ No newline at end of file diff --git a/shadowsocks-csharp/View/LogForm.Designer.cs b/shadowsocks-csharp/View/LogForm.Designer.cs deleted file mode 100644 index 4a62262ab..000000000 --- a/shadowsocks-csharp/View/LogForm.Designer.cs +++ /dev/null @@ -1,342 +0,0 @@ -namespace Shadowsocks.View -{ - partial class LogForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea(); - System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend(); - System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series(); - System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series(); - this.LogMessageTextBox = new System.Windows.Forms.TextBox(); - this.MainMenu = new System.Windows.Forms.MainMenu(this.components); - this.FileMenuItem = new System.Windows.Forms.MenuItem(); - this.OpenLocationMenuItem = new System.Windows.Forms.MenuItem(); - this.ExitMenuItem = new System.Windows.Forms.MenuItem(); - this.ViewMenuItem = new System.Windows.Forms.MenuItem(); - this.ClearLogsMenuItem = new System.Windows.Forms.MenuItem(); - this.ChangeFontMenuItem = new System.Windows.Forms.MenuItem(); - this.WrapTextMenuItem = new System.Windows.Forms.MenuItem(); - this.TopMostMenuItem = new System.Windows.Forms.MenuItem(); - this.MenuItemSeparater = new System.Windows.Forms.MenuItem(); - this.ShowToolbarMenuItem = new System.Windows.Forms.MenuItem(); - this.TopMostCheckBox = new System.Windows.Forms.CheckBox(); - this.ChangeFontButton = new System.Windows.Forms.Button(); - this.ClearLogsButton = new System.Windows.Forms.Button(); - this.WrapTextCheckBox = new System.Windows.Forms.CheckBox(); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.ToolbarFlowLayoutPanel = new System.Windows.Forms.FlowLayoutPanel(); - this.splitContainer1 = new System.Windows.Forms.SplitContainer(); - this.trafficChart = new System.Windows.Forms.DataVisualization.Charting.Chart(); - this.tableLayoutPanel1.SuspendLayout(); - this.ToolbarFlowLayoutPanel.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); - this.splitContainer1.Panel1.SuspendLayout(); - this.splitContainer1.Panel2.SuspendLayout(); - this.splitContainer1.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.trafficChart)).BeginInit(); - this.SuspendLayout(); - // - // LogMessageTextBox - // - this.LogMessageTextBox.BackColor = System.Drawing.Color.Black; - this.LogMessageTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.LogMessageTextBox.Font = new System.Drawing.Font("Consolas", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.LogMessageTextBox.ForeColor = System.Drawing.Color.White; - this.LogMessageTextBox.Location = new System.Drawing.Point(0, 0); - this.LogMessageTextBox.MaxLength = 2147483647; - this.LogMessageTextBox.Multiline = true; - this.LogMessageTextBox.Name = "LogMessageTextBox"; - this.LogMessageTextBox.ReadOnly = true; - this.LogMessageTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this.LogMessageTextBox.Size = new System.Drawing.Size(378, 74); - this.LogMessageTextBox.TabIndex = 0; - // - // MainMenu - // - this.MainMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { - this.FileMenuItem, - this.ViewMenuItem}); - // - // FileMenuItem - // - this.FileMenuItem.Index = 0; - this.FileMenuItem.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { - this.OpenLocationMenuItem, - this.ExitMenuItem}); - this.FileMenuItem.Text = "&File"; - // - // OpenLocationMenuItem - // - this.OpenLocationMenuItem.Index = 0; - this.OpenLocationMenuItem.Text = "&Open Location"; - this.OpenLocationMenuItem.Click += new System.EventHandler(this.OpenLocationMenuItem_Click); - // - // ExitMenuItem - // - this.ExitMenuItem.Index = 1; - this.ExitMenuItem.Text = "E&xit"; - this.ExitMenuItem.Click += new System.EventHandler(this.ExitMenuItem_Click); - // - // ViewMenuItem - // - this.ViewMenuItem.Index = 1; - this.ViewMenuItem.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { - this.ClearLogsMenuItem, - this.ChangeFontMenuItem, - this.WrapTextMenuItem, - this.TopMostMenuItem, - this.MenuItemSeparater, - this.ShowToolbarMenuItem}); - this.ViewMenuItem.Text = "&View"; - // - // ClearLogsMenuItem - // - this.ClearLogsMenuItem.Index = 0; - this.ClearLogsMenuItem.Text = "&Clear Logs"; - this.ClearLogsMenuItem.Click += new System.EventHandler(this.ClearLogsMenuItem_Click); - // - // ChangeFontMenuItem - // - this.ChangeFontMenuItem.Index = 1; - this.ChangeFontMenuItem.Text = "Change &Font"; - this.ChangeFontMenuItem.Click += new System.EventHandler(this.ChangeFontMenuItem_Click); - // - // WrapTextMenuItem - // - this.WrapTextMenuItem.Index = 2; - this.WrapTextMenuItem.Text = "&Wrap Text"; - this.WrapTextMenuItem.Click += new System.EventHandler(this.WrapTextMenuItem_Click); - // - // TopMostMenuItem - // - this.TopMostMenuItem.Index = 3; - this.TopMostMenuItem.Text = "&Top Most"; - this.TopMostMenuItem.Click += new System.EventHandler(this.TopMostMenuItem_Click); - // - // MenuItemSeparater - // - this.MenuItemSeparater.Index = 4; - this.MenuItemSeparater.Text = "-"; - // - // ShowToolbarMenuItem - // - this.ShowToolbarMenuItem.Index = 5; - this.ShowToolbarMenuItem.Text = "&Show Toolbar"; - this.ShowToolbarMenuItem.Click += new System.EventHandler(this.ShowToolbarMenuItem_Click); - // - // TopMostCheckBox - // - this.TopMostCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left))); - this.TopMostCheckBox.AutoSize = true; - this.TopMostCheckBox.Location = new System.Drawing.Point(249, 3); - this.TopMostCheckBox.Name = "TopMostCheckBox"; - this.TopMostCheckBox.Size = new System.Drawing.Size(72, 23); - this.TopMostCheckBox.TabIndex = 3; - this.TopMostCheckBox.Text = "&Top Most"; - this.TopMostCheckBox.UseVisualStyleBackColor = true; - this.TopMostCheckBox.CheckedChanged += new System.EventHandler(this.TopMostCheckBox_CheckedChanged); - // - // ChangeFontButton - // - this.ChangeFontButton.AutoSize = true; - this.ChangeFontButton.Location = new System.Drawing.Point(84, 3); - this.ChangeFontButton.Name = "ChangeFontButton"; - this.ChangeFontButton.Size = new System.Drawing.Size(75, 23); - this.ChangeFontButton.TabIndex = 2; - this.ChangeFontButton.Text = "&Font"; - this.ChangeFontButton.UseVisualStyleBackColor = true; - this.ChangeFontButton.Click += new System.EventHandler(this.ChangeFontButton_Click); - // - // ClearLogsButton - // - this.ClearLogsButton.AutoSize = true; - this.ClearLogsButton.Location = new System.Drawing.Point(3, 3); - this.ClearLogsButton.Name = "ClearLogsButton"; - this.ClearLogsButton.Size = new System.Drawing.Size(75, 23); - this.ClearLogsButton.TabIndex = 1; - this.ClearLogsButton.Text = "&Clear Logs"; - this.ClearLogsButton.UseVisualStyleBackColor = true; - this.ClearLogsButton.Click += new System.EventHandler(this.ClearLogsButton_Click); - // - // WrapTextCheckBox - // - this.WrapTextCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left))); - this.WrapTextCheckBox.AutoSize = true; - this.WrapTextCheckBox.Location = new System.Drawing.Point(165, 3); - this.WrapTextCheckBox.Name = "WrapTextCheckBox"; - this.WrapTextCheckBox.Size = new System.Drawing.Size(78, 23); - this.WrapTextCheckBox.TabIndex = 0; - this.WrapTextCheckBox.Text = "&Wrap Text"; - this.WrapTextCheckBox.UseVisualStyleBackColor = true; - this.WrapTextCheckBox.CheckedChanged += new System.EventHandler(this.WrapTextCheckBox_CheckedChanged); - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.ColumnCount = 1; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.ToolbarFlowLayoutPanel, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.splitContainer1, 0, 1); - this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 2; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(384, 161); - this.tableLayoutPanel1.TabIndex = 2; - // - // ToolbarFlowLayoutPanel - // - this.ToolbarFlowLayoutPanel.AutoSize = true; - this.ToolbarFlowLayoutPanel.Controls.Add(this.ClearLogsButton); - this.ToolbarFlowLayoutPanel.Controls.Add(this.ChangeFontButton); - this.ToolbarFlowLayoutPanel.Controls.Add(this.WrapTextCheckBox); - this.ToolbarFlowLayoutPanel.Controls.Add(this.TopMostCheckBox); - this.ToolbarFlowLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; - this.ToolbarFlowLayoutPanel.Location = new System.Drawing.Point(3, 3); - this.ToolbarFlowLayoutPanel.Name = "ToolbarFlowLayoutPanel"; - this.ToolbarFlowLayoutPanel.Size = new System.Drawing.Size(378, 29); - this.ToolbarFlowLayoutPanel.TabIndex = 2; - // - // splitContainer1 - // - this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; - this.splitContainer1.Location = new System.Drawing.Point(3, 38); - this.splitContainer1.Name = "splitContainer1"; - this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal; - // - // splitContainer1.Panel1 - // - this.splitContainer1.Panel1.Controls.Add(this.LogMessageTextBox); - // - // splitContainer1.Panel2 - // - this.splitContainer1.Panel2.Controls.Add(this.trafficChart); - this.splitContainer1.Size = new System.Drawing.Size(378, 120); - this.splitContainer1.SplitterDistance = 74; - this.splitContainer1.TabIndex = 3; - // - // trafficChart - // - chartArea1.AxisX.LabelStyle.Enabled = false; - chartArea1.AxisX.MajorGrid.Interval = 5D; - chartArea1.AxisX.MajorGrid.LineColor = System.Drawing.Color.LightGray; - chartArea1.AxisX.MajorTickMark.Enabled = false; - chartArea1.AxisX.Maximum = 61D; - chartArea1.AxisX.Minimum = 1D; - chartArea1.AxisY.IntervalAutoMode = System.Windows.Forms.DataVisualization.Charting.IntervalAutoMode.VariableCount; - chartArea1.AxisY.LabelAutoFitMaxFontSize = 8; - chartArea1.AxisY.LabelStyle.Interval = 0D; - chartArea1.AxisY.MajorGrid.LineColor = System.Drawing.Color.LightGray; - chartArea1.AxisY.MajorTickMark.Enabled = false; - chartArea1.AxisY2.MajorGrid.LineColor = System.Drawing.Color.LightGray; - chartArea1.AxisY2.Minimum = 0D; - chartArea1.Name = "ChartArea1"; - this.trafficChart.ChartAreas.Add(chartArea1); - this.trafficChart.Dock = System.Windows.Forms.DockStyle.Fill; - legend1.MaximumAutoSize = 25F; - legend1.Name = "Legend1"; - this.trafficChart.Legends.Add(legend1); - this.trafficChart.Location = new System.Drawing.Point(0, 0); - this.trafficChart.Name = "trafficChart"; - this.trafficChart.Palette = System.Windows.Forms.DataVisualization.Charting.ChartColorPalette.None; - series1.BorderWidth = 2; - series1.ChartArea = "ChartArea1"; - series1.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline; - series1.Color = System.Drawing.Color.FromArgb(255, 128, 0); - series1.IsXValueIndexed = true; - series1.Legend = "Legend1"; - series1.Name = "Inbound"; - series2.BorderWidth = 2; - series2.ChartArea = "ChartArea1"; - series2.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline; - series2.Color = System.Drawing.Color.FromArgb(128, 128, 255); - series2.IsXValueIndexed = true; - series2.Legend = "Legend1"; - series2.Name = "Outbound"; - this.trafficChart.Series.Add(series1); - this.trafficChart.Series.Add(series2); - this.trafficChart.Size = new System.Drawing.Size(378, 42); - this.trafficChart.TabIndex = 0; - this.trafficChart.Text = "chart1"; - // - // LogForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(384, 161); - this.Controls.Add(this.tableLayoutPanel1); - this.Menu = this.MainMenu; - this.MinimumSize = new System.Drawing.Size(400, 200); - this.Name = "LogForm"; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "Log Viewer"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.LogForm_FormClosing); - this.Load += new System.EventHandler(this.LogForm_Load); - this.Shown += new System.EventHandler(this.LogForm_Shown); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.ToolbarFlowLayoutPanel.ResumeLayout(false); - this.ToolbarFlowLayoutPanel.PerformLayout(); - this.splitContainer1.Panel1.ResumeLayout(false); - this.splitContainer1.Panel1.PerformLayout(); - this.splitContainer1.Panel2.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); - this.splitContainer1.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this.trafficChart)).EndInit(); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.TextBox LogMessageTextBox; - private System.Windows.Forms.MainMenu MainMenu; - private System.Windows.Forms.MenuItem FileMenuItem; - private System.Windows.Forms.MenuItem OpenLocationMenuItem; - private System.Windows.Forms.MenuItem ExitMenuItem; - private System.Windows.Forms.CheckBox WrapTextCheckBox; - private System.Windows.Forms.Button ClearLogsButton; - private System.Windows.Forms.Button ChangeFontButton; - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.CheckBox TopMostCheckBox; - private System.Windows.Forms.MenuItem ViewMenuItem; - private System.Windows.Forms.MenuItem ClearLogsMenuItem; - private System.Windows.Forms.MenuItem ChangeFontMenuItem; - private System.Windows.Forms.MenuItem WrapTextMenuItem; - private System.Windows.Forms.MenuItem TopMostMenuItem; - private System.Windows.Forms.FlowLayoutPanel ToolbarFlowLayoutPanel; - private System.Windows.Forms.MenuItem MenuItemSeparater; - private System.Windows.Forms.MenuItem ShowToolbarMenuItem; - private System.Windows.Forms.SplitContainer splitContainer1; - private System.Windows.Forms.DataVisualization.Charting.Chart trafficChart; - } -} \ No newline at end of file diff --git a/shadowsocks-csharp/View/LogForm.cs b/shadowsocks-csharp/View/LogForm.cs deleted file mode 100644 index 9f5efbebc..000000000 --- a/shadowsocks-csharp/View/LogForm.cs +++ /dev/null @@ -1,447 +0,0 @@ -using System; -using System.Drawing; -using System.IO; -using System.Windows.Forms; -using System.Windows.Forms.DataVisualization.Charting; -using System.Collections.Generic; -using System.Linq; - -using Shadowsocks.Controller; -using Shadowsocks.Properties; -using Shadowsocks.Model; -using Shadowsocks.Util; -using System.Text; -using NLog; - -namespace Shadowsocks.View -{ - public partial class LogForm : Form - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - long lastOffset; - string filename; - Timer timer; - const int BACK_OFFSET = 65536; - ShadowsocksController controller; - - // global traffic update lock, make it static - private static readonly object _lock = new object(); - - #region Traffic Chart - Queue trafficInfoQueue = new Queue(); - const int queueMaxLength = 60; - long lastInbound, lastOutbound; - long maxSpeed = 0, lastMaxSpeed = 0; - const long minScale = 50; - BandwidthScaleInfo bandwidthScale; - List inboundPoints = new List(); - List outboundPoints = new List(); - TextAnnotation inboundAnnotation = new TextAnnotation(); - TextAnnotation outboundAnnotation = new TextAnnotation(); - #endregion - - public LogForm(ShadowsocksController controller) - { - this.controller = controller; - - InitializeComponent(); - Icon = Icon.FromHandle(Resources.ssw128.GetHicon()); - - var nLogConfig = NLogConfig.LoadXML(); - try - { - this.filename = nLogConfig.GetLogFileName(); - } - catch(Exception) - { - // failed to get the file name - } - if (string.IsNullOrEmpty(this.filename)) - { - LogMessageTextBox.AppendText("Cannot get the log file name from NLog config file. Please check if the nlog config file exists with corresponding XML nodes."); - } - - LogViewerConfig config = controller.GetCurrentConfiguration().logViewer; - - topMostTrigger = config.topMost; - wrapTextTrigger = config.wrapText; - toolbarTrigger = config.toolbarShown; - LogMessageTextBox.BackColor = config.BackgroundColor; - LogMessageTextBox.ForeColor = config.TextColor; - LogMessageTextBox.Font = config.Font; - - controller.TrafficChanged += controller_TrafficChanged; - - UpdateTexts(); - } - - private void UpdateTrafficChart() - { - lock (_lock) - { - if (trafficInfoQueue.Count == 0) - return; - - inboundPoints.Clear(); - outboundPoints.Clear(); - maxSpeed = 0; - foreach (var trafficInfo in trafficInfoQueue) - { - inboundPoints.Add(trafficInfo.inbound); - outboundPoints.Add(trafficInfo.outbound); - maxSpeed = Math.Max(maxSpeed, Math.Max(trafficInfo.inbound, trafficInfo.outbound)); - } - lastInbound = trafficInfoQueue.Last().inbound; - lastOutbound = trafficInfoQueue.Last().outbound; - } - - if (maxSpeed > 0) - { - lastMaxSpeed -= lastMaxSpeed / 32; - maxSpeed = Math.Max(minScale, Math.Max(maxSpeed, lastMaxSpeed)); - lastMaxSpeed = maxSpeed; - } - else - { - maxSpeed = lastMaxSpeed = minScale; - } - - bandwidthScale = Utils.GetBandwidthScale(maxSpeed); - - // re-scale the original data points, since it is List, .ForEach does not work - inboundPoints = inboundPoints.Select(p => p / bandwidthScale.unit).ToList(); - outboundPoints = outboundPoints.Select(p => p / bandwidthScale.unit).ToList(); - - if (trafficChart.IsHandleCreated) - { - trafficChart.Series["Inbound"].Points.DataBindY(inboundPoints); - trafficChart.Series["Outbound"].Points.DataBindY(outboundPoints); - trafficChart.ChartAreas[0].AxisY.LabelStyle.Format = "{0:0.##} " + bandwidthScale.unitName; - trafficChart.ChartAreas[0].AxisY.Maximum = bandwidthScale.value; - inboundAnnotation.AnchorDataPoint = trafficChart.Series["Inbound"].Points.Last(); - inboundAnnotation.Text = Utils.FormatBandwidth(lastInbound); - outboundAnnotation.AnchorDataPoint = trafficChart.Series["Outbound"].Points.Last(); - outboundAnnotation.Text = Utils.FormatBandwidth(lastOutbound); - trafficChart.Annotations.Clear(); - trafficChart.Annotations.Add(inboundAnnotation); - trafficChart.Annotations.Add(outboundAnnotation); - } - } - - private void controller_TrafficChanged(object sender, EventArgs e) - { - lock (_lock) - { - if (trafficInfoQueue.Count == 0) - { - // Init an empty queue - for (int i = 0; i < queueMaxLength; i++) - { - trafficInfoQueue.Enqueue(new TrafficInfo(0, 0)); - } - - foreach (var trafficPerSecond in controller.trafficPerSecondQueue) - { - trafficInfoQueue.Enqueue(new TrafficInfo(trafficPerSecond.inboundIncreasement, - trafficPerSecond.outboundIncreasement)); - if (trafficInfoQueue.Count > queueMaxLength) - trafficInfoQueue.Dequeue(); - } - } - else - { - var lastTraffic = controller.trafficPerSecondQueue.Last(); - trafficInfoQueue.Enqueue(new TrafficInfo(lastTraffic.inboundIncreasement, - lastTraffic.outboundIncreasement)); - if (trafficInfoQueue.Count > queueMaxLength) - trafficInfoQueue.Dequeue(); - } - } - } - - private void UpdateTexts() - { - I18N.TranslateForm(this); - trafficChart.Series["Inbound"].LegendText = I18N.GetString("Inbound"); - trafficChart.Series["Outbound"].LegendText = I18N.GetString("Outbound"); - } - - private void Timer_Tick(object sender, EventArgs e) - { - UpdateContent(); - UpdateTrafficChart(); - } - - private void InitContent() - { - if (string.IsNullOrEmpty(filename) || !File.Exists(filename)) - return; - using (StreamReader reader = new StreamReader(new FileStream(filename, - FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - if (reader.BaseStream.Length > BACK_OFFSET) - { - reader.BaseStream.Seek(-BACK_OFFSET, SeekOrigin.End); - reader.ReadLine(); - } - - string line = ""; - StringBuilder appendText = new StringBuilder(1024); - while ((line = reader.ReadLine()) != null) - appendText.AppendLine(line); - - LogMessageTextBox.AppendText(appendText.ToString()); - LogMessageTextBox.ScrollToCaret(); - - lastOffset = reader.BaseStream.Position; - } - } - - private void UpdateContent() - { - this.Text = I18N.GetString("Log Viewer") + - $" [in: {Utils.FormatBytes(controller.InboundCounter)}, out: {Utils.FormatBytes(controller.OutboundCounter)}]"; - - if (string.IsNullOrEmpty(filename) || !File.Exists(filename)) - return; - try - { - using (StreamReader reader = new StreamReader(new FileStream(filename, - FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - reader.BaseStream.Seek(lastOffset, SeekOrigin.Begin); - - string line = ""; - bool changed = false; - StringBuilder appendText = new StringBuilder(128); - while ((line = reader.ReadLine()) != null) - { - changed = true; - appendText.AppendLine(line); - } - - if (changed) - { - LogMessageTextBox.AppendText(appendText.ToString()); - LogMessageTextBox.ScrollToCaret(); - } - - lastOffset = reader.BaseStream.Position; - } - } - catch (FileNotFoundException) - { - } - } - - private void LogForm_Load(object sender, EventArgs e) - { - InitContent(); - - timer = new Timer(); - timer.Interval = 100; - timer.Tick += Timer_Tick; - timer.Start(); - - LogViewerConfig config = controller.GetCurrentConfiguration().logViewer; - - Height = config.Height; - Width = config.Width; - Top = config.BestTop; - Left = config.BestLeft; - if (config.Maximized) - { - WindowState = FormWindowState.Maximized; - } - - topMostTriggerLock = true; - TopMost = TopMostMenuItem.Checked = TopMostCheckBox.Checked = topMostTrigger; - topMostTriggerLock = false; - - wrapTextTriggerLock = true; - LogMessageTextBox.WordWrap = WrapTextMenuItem.Checked = WrapTextCheckBox.Checked = wrapTextTrigger; - wrapTextTriggerLock = false; - - ToolbarFlowLayoutPanel.Visible = ShowToolbarMenuItem.Checked = toolbarTrigger; - } - - private void LogForm_FormClosing(object sender, FormClosingEventArgs e) - { - timer.Stop(); - controller.TrafficChanged -= controller_TrafficChanged; - LogViewerConfig config = controller.GetCurrentConfiguration().logViewer; - - config.topMost = topMostTrigger; - config.wrapText = wrapTextTrigger; - config.toolbarShown = toolbarTrigger; - config.Font = LogMessageTextBox.Font; - config.BackgroundColor = LogMessageTextBox.BackColor; - config.TextColor = LogMessageTextBox.ForeColor; - if (WindowState != FormWindowState.Minimized && !(config.Maximized = WindowState == FormWindowState.Maximized)) - { - config.Top = Top; - config.Left = Left; - config.Height = Height; - config.Width = Width; - } - controller.SaveLogViewerConfig(config); - } - - private void OpenLocationMenuItem_Click(object sender, EventArgs e) - { - string argument = "/select, \"" + filename + "\""; - logger.Debug(argument); - System.Diagnostics.Process.Start("explorer.exe", argument); - } - - private void ExitMenuItem_Click(object sender, EventArgs e) - { - Close(); - } - - private void LogForm_Shown(object sender, EventArgs e) - { - LogMessageTextBox.ScrollToCaret(); - } - - #region Clean up the content in LogMessageTextBox. - private void DoClearLogs() - { - try - { - File.Delete(filename); - } - catch { } - lastOffset = 0; - LogMessageTextBox.Clear(); - } - - private void ClearLogsMenuItem_Click(object sender, EventArgs e) - { - DoClearLogs(); - } - - private void ClearLogsButton_Click(object sender, EventArgs e) - { - DoClearLogs(); - } - #endregion - - #region Change the font settings applied in LogMessageTextBox. - private void DoChangeFont() - { - try - { - FontDialog fd = new FontDialog(); - fd.Font = LogMessageTextBox.Font; - if (fd.ShowDialog() == DialogResult.OK) - { - LogMessageTextBox.Font = new Font(fd.Font.FontFamily, fd.Font.Size, fd.Font.Style); - } - } - catch (Exception ex) - { - logger.LogUsefulException(ex); - MessageBox.Show(ex.Message); - } - } - - private void ChangeFontMenuItem_Click(object sender, EventArgs e) - { - DoChangeFont(); - } - - private void ChangeFontButton_Click(object sender, EventArgs e) - { - DoChangeFont(); - } - #endregion - - #region Trigger the log messages to wrapable, or not. - bool wrapTextTrigger = false; - bool wrapTextTriggerLock = false; - - private void TriggerWrapText() - { - wrapTextTriggerLock = true; - - wrapTextTrigger = !wrapTextTrigger; - LogMessageTextBox.WordWrap = wrapTextTrigger; - LogMessageTextBox.ScrollToCaret(); - WrapTextMenuItem.Checked = WrapTextCheckBox.Checked = wrapTextTrigger; - - wrapTextTriggerLock = false; - } - - private void WrapTextMenuItem_Click(object sender, EventArgs e) - { - if (!wrapTextTriggerLock) - { - TriggerWrapText(); - } - } - - private void WrapTextCheckBox_CheckedChanged(object sender, EventArgs e) - { - if (!wrapTextTriggerLock) - { - TriggerWrapText(); - } - } - #endregion - - #region Trigger the window to top most, or not. - bool topMostTrigger = false; - bool topMostTriggerLock = false; - - private void TriggerTopMost() - { - topMostTriggerLock = true; - - topMostTrigger = !topMostTrigger; - TopMost = topMostTrigger; - TopMostMenuItem.Checked = TopMostCheckBox.Checked = topMostTrigger; - - topMostTriggerLock = false; - } - - private void TopMostCheckBox_CheckedChanged(object sender, EventArgs e) - { - if (!topMostTriggerLock) - { - TriggerTopMost(); - } - } - - private void TopMostMenuItem_Click(object sender, EventArgs e) - { - if (!topMostTriggerLock) - { - TriggerTopMost(); - } - } - #endregion - - private bool toolbarTrigger = false; - - private void ShowToolbarMenuItem_Click(object sender, EventArgs e) - { - toolbarTrigger = !toolbarTrigger; - ToolbarFlowLayoutPanel.Visible = toolbarTrigger; - ShowToolbarMenuItem.Checked = toolbarTrigger; - } - - private class TrafficInfo - { - public long inbound; - public long outbound; - - public TrafficInfo(long inbound, long outbound) - { - this.inbound = inbound; - this.outbound = outbound; - } - } - } -} diff --git a/shadowsocks-csharp/View/LogForm.resx b/shadowsocks-csharp/View/LogForm.resx deleted file mode 100644 index 1f26a6033..000000000 --- a/shadowsocks-csharp/View/LogForm.resx +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - True - - - 17, 17 - - - True - - - True - - - True - - - True - - - True - - - True - - - True - - - 39 - - \ No newline at end of file diff --git a/shadowsocks-csharp/View/MenuViewController.cs b/shadowsocks-csharp/View/MenuViewController.cs deleted file mode 100644 index 71f0b76c8..000000000 --- a/shadowsocks-csharp/View/MenuViewController.cs +++ /dev/null @@ -1,1003 +0,0 @@ -using NLog; -using Shadowsocks.Controller; -using Shadowsocks.Localization; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using Shadowsocks.Views; -using System; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Text; -using System.Windows.Forms; -using System.Windows.Forms.Integration; -using System.Windows.Threading; -using ZXing; -using ZXing.Common; -using ZXing.QrCode; - -namespace Shadowsocks.View -{ - public class MenuViewController - { - private readonly Logger logger = LogManager.GetCurrentClassLogger(); - - private ShadowsocksController controller; - public UpdateChecker updateChecker; - - private NotifyIcon _notifyIcon; - private Icon icon, icon_in, icon_out, icon_both, previousIcon; - - private bool _isStartupCheck; - private string _urlToOpen; - - private ContextMenu contextMenu1; - private MenuItem disableItem; - private MenuItem AutoStartupItem; - private MenuItem ProtocolHandlerItem; - private MenuItem ShareOverLANItem; - private MenuItem SeperatorItem; - private MenuItem ConfigItem; - private MenuItem ServersItem; - private MenuItem globalModeItem; - private MenuItem PACModeItem; - private MenuItem localPACItem; - private MenuItem onlinePACItem; - private MenuItem editLocalPACItem; - private MenuItem updateFromGeositeItem; - private MenuItem editGFWUserRuleItem; - private MenuItem editOnlinePACItem; - private MenuItem secureLocalPacUrlToggleItem; - private MenuItem regenerateLocalPacOnUpdateItem; - private MenuItem autoCheckUpdatesToggleItem; - private MenuItem checkPreReleaseToggleItem; - private MenuItem proxyItem; - private MenuItem hotKeyItem; - private MenuItem VerboseLoggingToggleItem; - private MenuItem ShowPluginOutputToggleItem; - private MenuItem WriteI18NFileItem; - private MenuItem onlineConfigItem; - - private ConfigForm configForm; - private LogForm logForm; - - private System.Windows.Window serverSharingWindow; - private System.Windows.Window hotkeysWindow; - private System.Windows.Window forwardProxyWindow; - private System.Windows.Window onlineConfigWindow; - - // color definition for icon color transformation - private readonly Color colorMaskBlue = Color.FromArgb(255, 25, 125, 191); - private readonly Color colorMaskDarkSilver = Color.FromArgb(128, 192, 192, 192); - private readonly Color colorMaskLightSilver = Color.FromArgb(192, 192, 192); - private readonly Color colorMaskEclipse = Color.FromArgb(192, 64, 64, 64); - - public MenuViewController(ShadowsocksController controller) - { - this.controller = controller; - - LoadMenu(); - - controller.EnableStatusChanged += controller_EnableStatusChanged; - controller.ConfigChanged += controller_ConfigChanged; - controller.PACFileReadyToOpen += controller_FileReadyToOpen; - controller.UserRuleFileReadyToOpen += controller_FileReadyToOpen; - controller.ShareOverLANStatusChanged += controller_ShareOverLANStatusChanged; - controller.VerboseLoggingStatusChanged += controller_VerboseLoggingStatusChanged; - controller.ShowPluginOutputChanged += controller_ShowPluginOutputChanged; - controller.EnableGlobalChanged += controller_EnableGlobalChanged; - controller.Errored += controller_Errored; - controller.UpdatePACFromGeositeCompleted += controller_UpdatePACFromGeositeCompleted; - controller.UpdatePACFromGeositeError += controller_UpdatePACFromGeositeError; - - _notifyIcon = new NotifyIcon(); - UpdateTrayIconAndNotifyText(); - _notifyIcon.Visible = true; - _notifyIcon.ContextMenu = contextMenu1; - _notifyIcon.BalloonTipClicked += notifyIcon1_BalloonTipClicked; - _notifyIcon.MouseClick += notifyIcon1_Click; - _notifyIcon.MouseDoubleClick += notifyIcon1_DoubleClick; - _notifyIcon.BalloonTipClosed += _notifyIcon_BalloonTipClosed; - controller.TrafficChanged += controller_TrafficChanged; - - updateChecker = new UpdateChecker(); - updateChecker.CheckUpdateCompleted += updateChecker_CheckUpdateCompleted; - - LoadCurrentConfiguration(); - - Configuration config = controller.GetCurrentConfiguration(); - - if (config.firstRun) - { - ShowConfigForm(); - } - else if (config.autoCheckUpdate) - { - _isStartupCheck = true; - Dispatcher.CurrentDispatcher.Invoke(() => updateChecker.CheckForVersionUpdate(3000)); - } - } - - #region Tray Icon - - private void UpdateTrayIconAndNotifyText() - { - Configuration config = controller.GetCurrentConfiguration(); - bool enabled = config.enabled; - bool global = config.global; - - Color colorMask = SelectColorMask(enabled, global); - Size iconSize = SelectIconSize(); - - UpdateIconSet(colorMask, iconSize, out icon, out icon_in, out icon_out, out icon_both); - - previousIcon = icon; - _notifyIcon.Icon = previousIcon; - - string serverInfo = null; - if (controller.GetCurrentStrategy() != null) - { - serverInfo = controller.GetCurrentStrategy().Name; - } - else - { - serverInfo = config.GetCurrentServer().ToString(); - } - // show more info by hacking the P/Invoke declaration for NOTIFYICONDATA inside Windows Forms - string text = I18N.GetString("Shadowsocks") + " " + UpdateChecker.Version + "\n" + - (enabled ? - I18N.GetString("System Proxy On: ") + (global ? I18N.GetString("Global") : I18N.GetString("PAC")) : - I18N.GetString("Running: Port {0}", config.localPort)) // this feedback is very important because they need to know Shadowsocks is running - + "\n" + serverInfo; - if (text.Length > 127) - { - text = text.Substring(0, 126 - 3) + "..."; - } - ViewUtils.SetNotifyIconText(_notifyIcon, text); - } - - /// - /// Determine the icon size based on the screen DPI. - /// - /// - /// https://stackoverflow.com/a/40851713/2075611 - private Size SelectIconSize() - { - Size size = new Size(32, 32); - int dpi = ViewUtils.GetScreenDpi(); - if (dpi < 97) - { - // dpi = 96; - size = new Size(16, 16); - } - else if (dpi < 121) - { - // dpi = 120; - size = new Size(20, 20); - } - else if (dpi < 145) - { - // dpi = 144; - size = new Size(24, 24); - } - else - { - // dpi = 168; - size = new Size(28, 28); - } - return size; - } - - private Color SelectColorMask(bool isProxyEnabled, bool isGlobalProxy) - { - Color colorMask = Color.White; - - Utils.WindowsThemeMode currentWindowsThemeMode = Utils.GetWindows10SystemThemeSetting(); - - if (isProxyEnabled) - { - if (isGlobalProxy) // global - { - colorMask = colorMaskBlue; - } - else // PAC - { - if (currentWindowsThemeMode == Utils.WindowsThemeMode.Light) - { - colorMask = colorMaskEclipse; - } - } - } - else // disabled - { - if (currentWindowsThemeMode == Utils.WindowsThemeMode.Light) - { - colorMask = colorMaskDarkSilver; - } - else - { - colorMask = colorMaskLightSilver; - } - } - - return colorMask; - } - - private void UpdateIconSet(Color colorMask, Size size, - out Icon icon, out Icon icon_in, out Icon icon_out, out Icon icon_both) - { - Bitmap iconBitmap; - - // generate the base icon - iconBitmap = ViewUtils.ChangeBitmapColor(Resources.ss32Fill, colorMask); - iconBitmap = ViewUtils.AddBitmapOverlay(iconBitmap, Resources.ss32Outline); - - icon = Icon.FromHandle(ViewUtils.ResizeBitmap(iconBitmap, size.Width, size.Height).GetHicon()); - icon_in = Icon.FromHandle(ViewUtils.ResizeBitmap(ViewUtils.AddBitmapOverlay(iconBitmap, Resources.ss32In), size.Width, size.Height).GetHicon()); - icon_out = Icon.FromHandle(ViewUtils.ResizeBitmap(ViewUtils.AddBitmapOverlay(iconBitmap, Resources.ss32In), size.Width, size.Height).GetHicon()); - icon_both = Icon.FromHandle(ViewUtils.ResizeBitmap(ViewUtils.AddBitmapOverlay(iconBitmap, Resources.ss32In, Resources.ss32Out), size.Width, size.Height).GetHicon()); - } - - #endregion - - #region MenuItems and MenuGroups - - private MenuItem CreateMenuItem(string text, EventHandler click) - { - return new MenuItem(I18N.GetString(text), click); - } - - private MenuItem CreateMenuGroup(string text, MenuItem[] items) - { - return new MenuItem(I18N.GetString(text), items); - } - - private void LoadMenu() - { - this.contextMenu1 = new ContextMenu(new MenuItem[] { - CreateMenuGroup("System Proxy", new MenuItem[] { - this.disableItem = CreateMenuItem("Disable", new EventHandler(this.EnableItem_Click)), - this.PACModeItem = CreateMenuItem("PAC", new EventHandler(this.PACModeItem_Click)), - this.globalModeItem = CreateMenuItem("Global", new EventHandler(this.GlobalModeItem_Click)) - }), - this.ServersItem = CreateMenuGroup("Servers", new MenuItem[] { - this.SeperatorItem = new MenuItem("-"), - this.ConfigItem = CreateMenuItem("Edit Servers...", new EventHandler(this.Config_Click)), - new MenuItem("-"), - CreateMenuItem("Share Server Config...", new EventHandler(this.QRCodeItem_Click)), - CreateMenuItem("Scan QRCode from Screen...", new EventHandler(this.ScanQRCodeItem_Click)), - CreateMenuItem("Import URL from Clipboard...", new EventHandler(this.ImportURLItem_Click)) - }), - CreateMenuGroup("PAC ", new MenuItem[] { - this.localPACItem = CreateMenuItem("Local PAC", new EventHandler(this.LocalPACItem_Click)), - this.onlinePACItem = CreateMenuItem("Online PAC", new EventHandler(this.OnlinePACItem_Click)), - new MenuItem("-"), - this.editLocalPACItem = CreateMenuItem("Edit Local PAC File...", new EventHandler(this.EditPACFileItem_Click)), - this.updateFromGeositeItem = CreateMenuItem("Update Local PAC from Geosite", new EventHandler(this.UpdatePACFromGeositeItem_Click)), - this.editGFWUserRuleItem = CreateMenuItem("Edit User Rule for Geosite...", new EventHandler(this.EditUserRuleFileForGeositeItem_Click)), - this.secureLocalPacUrlToggleItem = CreateMenuItem("Secure Local PAC", new EventHandler(this.SecureLocalPacUrlToggleItem_Click)), - this.regenerateLocalPacOnUpdateItem = CreateMenuItem("Regenerate local PAC on version update", new EventHandler(this.RegenerateLocalPacOnUpdateItem_Click)), - CreateMenuItem("Copy Local PAC URL", new EventHandler(this.CopyLocalPacUrlItem_Click)), - this.editOnlinePACItem = CreateMenuItem("Edit Online PAC URL...", new EventHandler(this.UpdateOnlinePACURLItem_Click)), - }), - this.proxyItem = CreateMenuItem("Forward Proxy...", new EventHandler(this.proxyItem_Click)), - this.onlineConfigItem = CreateMenuItem("Online Config...", new EventHandler(this.OnlineConfig_Click)), - new MenuItem("-"), - this.AutoStartupItem = CreateMenuItem("Start on Boot", new EventHandler(this.AutoStartupItem_Click)), - this.ProtocolHandlerItem = CreateMenuItem("Associate ss:// Links", new EventHandler(this.ProtocolHandlerItem_Click)), - this.ShareOverLANItem = CreateMenuItem("Allow other Devices to connect", new EventHandler(this.ShareOverLANItem_Click)), - new MenuItem("-"), - this.hotKeyItem = CreateMenuItem("Edit Hotkeys...", new EventHandler(this.hotKeyItem_Click)), - CreateMenuGroup("Help", new MenuItem[] { - CreateMenuItem("Show Logs...", new EventHandler(this.ShowLogItem_Click)), - this.VerboseLoggingToggleItem = CreateMenuItem( "Verbose Logging", new EventHandler(this.VerboseLoggingToggleItem_Click) ), - this.ShowPluginOutputToggleItem = CreateMenuItem("Show Plugin Output", new EventHandler(this.ShowPluginOutputToggleItem_Click)), - this.WriteI18NFileItem = CreateMenuItem("Write translation template",new EventHandler(WriteI18NFileItem_Click)), - CreateMenuGroup("Updates...", new MenuItem[] { - CreateMenuItem("Check for Updates...", new EventHandler(this.checkUpdatesItem_Click)), - new MenuItem("-"), - this.autoCheckUpdatesToggleItem = CreateMenuItem("Check for Updates at Startup", new EventHandler(this.autoCheckUpdatesToggleItem_Click)), - this.checkPreReleaseToggleItem = CreateMenuItem("Check Pre-release Version", new EventHandler(this.checkPreReleaseToggleItem_Click)), - }), - CreateMenuItem("About...", new EventHandler(this.AboutItem_Click)), - }), - new MenuItem("-"), - CreateMenuItem("Quit", new EventHandler(this.Quit_Click)) - }); - } - - #endregion - - private void controller_TrafficChanged(object sender, EventArgs e) - { - if (icon == null) - return; - - Icon newIcon; - - bool hasInbound = controller.trafficPerSecondQueue.Last().inboundIncreasement > 0; - bool hasOutbound = controller.trafficPerSecondQueue.Last().outboundIncreasement > 0; - - if (hasInbound && hasOutbound) - newIcon = icon_both; - else if (hasInbound) - newIcon = icon_in; - else if (hasOutbound) - newIcon = icon_out; - else - newIcon = icon; - - if (newIcon != this.previousIcon) - { - this.previousIcon = newIcon; - _notifyIcon.Icon = newIcon; - } - } - - void controller_Errored(object sender, ErrorEventArgs e) - { - MessageBox.Show(e.GetException().ToString(), I18N.GetString("Shadowsocks Error: {0}", e.GetException().Message)); - } - - private void controller_ConfigChanged(object sender, EventArgs e) - { - LoadCurrentConfiguration(); - UpdateTrayIconAndNotifyText(); - } - - private void LoadCurrentConfiguration() - { - Configuration config = controller.GetCurrentConfiguration(); - UpdateServersMenu(); - UpdateSystemProxyItemsEnabledStatus(config); - ShareOverLANItem.Checked = config.shareOverLan; - VerboseLoggingToggleItem.Checked = config.isVerboseLogging; - ShowPluginOutputToggleItem.Checked = config.showPluginOutput; - AutoStartupItem.Checked = AutoStartup.Check(); - ProtocolHandlerItem.Checked = ProtocolHandler.Check(); - onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; - localPACItem.Checked = !onlinePACItem.Checked; - secureLocalPacUrlToggleItem.Checked = config.secureLocalPac; - regenerateLocalPacOnUpdateItem.Checked = config.regeneratePacOnUpdate; - UpdatePACItemsEnabledStatus(); - UpdateUpdateMenu(); - } - - #region Forms - - private void ShowConfigForm() - { - if (configForm != null) - { - configForm.Activate(); - } - else - { - configForm = new ConfigForm(controller); - configForm.Show(); - configForm.Activate(); - configForm.FormClosed += configForm_FormClosed; - } - } - - private void ShowLogForm() - { - if (logForm != null) - { - logForm.Activate(); - } - else - { - logForm = new LogForm(controller); - logForm.Show(); - logForm.Activate(); - logForm.FormClosed += logForm_FormClosed; - } - } - - void logForm_FormClosed(object sender, FormClosedEventArgs e) - { - logForm.Dispose(); - logForm = null; - } - - void configForm_FormClosed(object sender, FormClosedEventArgs e) - { - configForm.Dispose(); - configForm = null; - var config = controller.GetCurrentConfiguration(); - if (config.firstRun) - { - CheckUpdateForFirstRun(); - ShowBalloonTip( - I18N.GetString("Shadowsocks is here"), - I18N.GetString("You can turn on/off Shadowsocks in the context menu"), - ToolTipIcon.Info, - 0 - ); - config.firstRun = false; - } - } - - #endregion - - #region Misc - - void ShowBalloonTip(string title, string content, ToolTipIcon icon, int timeout) - { - _notifyIcon.BalloonTipTitle = title; - _notifyIcon.BalloonTipText = content; - _notifyIcon.BalloonTipIcon = icon; - _notifyIcon.ShowBalloonTip(timeout); - } - - void notifyIcon1_BalloonTipClicked(object sender, EventArgs e) - { - } - - private void _notifyIcon_BalloonTipClosed(object sender, EventArgs e) - { - } - - private void notifyIcon1_Click(object sender, MouseEventArgs e) - { - UpdateTrayIconAndNotifyText(); - if (e.Button == MouseButtons.Middle) - { - ShowLogForm(); - } - } - - private void notifyIcon1_DoubleClick(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - ShowConfigForm(); - } - } - - private void CheckUpdateForFirstRun() - { - Configuration config = controller.GetCurrentConfiguration(); - if (config.firstRun) - return; - _isStartupCheck = true; - Dispatcher.CurrentDispatcher.Invoke(() => updateChecker.CheckForVersionUpdate(3000)); - } - - public void ShowLogForm_HotKey() - { - ShowLogForm(); - } - - #endregion - - #region Main menu - - void controller_ShareOverLANStatusChanged(object sender, EventArgs e) - { - ShareOverLANItem.Checked = controller.GetCurrentConfiguration().shareOverLan; - } - - private void proxyItem_Click(object sender, EventArgs e) - { - if (forwardProxyWindow == null) - { - forwardProxyWindow = new System.Windows.Window() - { - Title = LocalizationProvider.GetLocalizedValue("ForwardProxy"), - Height = 400, - Width = 280, - MinHeight = 400, - MinWidth = 280, - Content = new ForwardProxyView() - }; - forwardProxyWindow.Closed += ForwardProxyWindow_Closed; - ElementHost.EnableModelessKeyboardInterop(forwardProxyWindow); - forwardProxyWindow.Show(); - } - forwardProxyWindow.Activate(); - } - - private void ForwardProxyWindow_Closed(object sender, EventArgs e) - { - forwardProxyWindow = null; - } - - public void CloseForwardProxyWindow() => forwardProxyWindow.Close(); - - private void OnlineConfig_Click(object sender, EventArgs e) - { - if (onlineConfigWindow == null) - { - onlineConfigWindow = new System.Windows.Window() - { - Title = LocalizationProvider.GetLocalizedValue("OnlineConfigDelivery"), - Height = 510, - Width = 480, - MinHeight = 510, - MinWidth = 480, - Content = new OnlineConfigView() - }; - onlineConfigWindow.Closed += OnlineConfigWindow_Closed; - ElementHost.EnableModelessKeyboardInterop(onlineConfigWindow); - onlineConfigWindow.Show(); - } - onlineConfigWindow.Activate(); - } - - private void OnlineConfigWindow_Closed(object sender, EventArgs e) - { - onlineConfigWindow = null; - } - - private void hotKeyItem_Click(object sender, EventArgs e) - { - if (hotkeysWindow == null) - { - hotkeysWindow = new System.Windows.Window() - { - Title = LocalizationProvider.GetLocalizedValue("Hotkeys"), - Height = 260, - Width = 320, - MinHeight = 260, - MinWidth = 320, - Content = new HotkeysView() - }; - hotkeysWindow.Closed += HotkeysWindow_Closed; - ElementHost.EnableModelessKeyboardInterop(hotkeysWindow); - hotkeysWindow.Show(); - } - hotkeysWindow.Activate(); - } - - private void HotkeysWindow_Closed(object sender, EventArgs e) - { - hotkeysWindow = null; - } - - public void CloseHotkeysWindow() => hotkeysWindow.Close(); - - private void ShareOverLANItem_Click(object sender, EventArgs e) - { - ShareOverLANItem.Checked = !ShareOverLANItem.Checked; - controller.ToggleShareOverLAN(ShareOverLANItem.Checked); - } - - private void AutoStartupItem_Click(object sender, EventArgs e) - { - AutoStartupItem.Checked = !AutoStartupItem.Checked; - if (!AutoStartup.Set(AutoStartupItem.Checked)) - { - MessageBox.Show(I18N.GetString("Failed to update registry")); - } - LoadCurrentConfiguration(); - } - - private void ProtocolHandlerItem_Click(object sender, EventArgs e) - { - ProtocolHandlerItem.Checked = !ProtocolHandlerItem.Checked; - if (!ProtocolHandler.Set(ProtocolHandlerItem.Checked)) - { - MessageBox.Show(I18N.GetString("Failed to update registry")); - } - LoadCurrentConfiguration(); - } - - private void Quit_Click(object sender, EventArgs e) - { - controller.Stop(); - _notifyIcon.Visible = false; - Application.Exit(); - } - - #endregion - - #region System proxy - - private void controller_EnableStatusChanged(object sender, EventArgs e) - { - disableItem.Checked = !controller.GetCurrentConfiguration().enabled; - } - - private void EnableItem_Click(object sender, EventArgs e) - { - controller.ToggleEnable(false); - Configuration config = controller.GetCurrentConfiguration(); - UpdateSystemProxyItemsEnabledStatus(config); - } - - void controller_EnableGlobalChanged(object sender, EventArgs e) - { - globalModeItem.Checked = controller.GetCurrentConfiguration().global; - PACModeItem.Checked = !globalModeItem.Checked; - } - - private void UpdateSystemProxyItemsEnabledStatus(Configuration config) - { - disableItem.Checked = !config.enabled; - if (!config.enabled) - { - globalModeItem.Checked = false; - PACModeItem.Checked = false; - } - else - { - globalModeItem.Checked = config.global; - PACModeItem.Checked = !config.global; - } - } - - private void GlobalModeItem_Click(object sender, EventArgs e) - { - controller.ToggleEnable(true); - controller.ToggleGlobal(true); - Configuration config = controller.GetCurrentConfiguration(); - UpdateSystemProxyItemsEnabledStatus(config); - } - - private void PACModeItem_Click(object sender, EventArgs e) - { - controller.ToggleEnable(true); - controller.ToggleGlobal(false); - Configuration config = controller.GetCurrentConfiguration(); - UpdateSystemProxyItemsEnabledStatus(config); - } - - #endregion - - #region Server - - private void UpdateServersMenu() - { - var items = ServersItem.MenuItems; - while (items[0] != SeperatorItem) - { - items.RemoveAt(0); - } - int strategyCount = 0; - foreach (var strategy in controller.GetStrategies()) - { - MenuItem item = new MenuItem(strategy.Name); - item.Tag = strategy.ID; - item.Click += AStrategyItem_Click; - items.Add(strategyCount, item); - strategyCount++; - } - - // user wants a seperator item between strategy and servers menugroup - items.Add(strategyCount++, new MenuItem("-")); - - int maxCount = 20; - int serverCount = 0; - bool overflow = false; - bool needAdd = true; - - Configuration configuration = controller.GetCurrentConfiguration(); - for (int i = 0; i < configuration.configs.Count; i++) - { - try - { - if (overflow) - { - needAdd = configuration.index >= i; - if (needAdd) - { - i = configuration.index; - } - } - - if (needAdd) - { - var server = configuration.configs[i]; - Configuration.CheckServer(server); - var item = new MenuItem(server.ToString()); - item.Tag = i; - item.Click += AServerItem_Click; - items.Add(strategyCount + serverCount, item); - serverCount++; - } - - if (overflow) - { - items.Add(strategyCount + serverCount, new MenuItem($"... more than {maxCount} (total {configuration.configs.Count})", Config_Click)); - break; - } - - overflow = serverCount > maxCount; - } - catch - { - } - } - - foreach (MenuItem item in items) - { - if (item.Tag != null && (item.Tag.ToString() == configuration.index.ToString() || item.Tag.ToString() == configuration.strategy)) - { - item.Checked = true; - } - } - } - - private void AServerItem_Click(object sender, EventArgs e) - { - MenuItem item = (MenuItem)sender; - controller.SelectServerIndex((int)item.Tag); - } - - private void AStrategyItem_Click(object sender, EventArgs e) - { - MenuItem item = (MenuItem)sender; - controller.SelectStrategy((string)item.Tag); - } - - private void Config_Click(object sender, EventArgs e) - { - ShowConfigForm(); - } - - void openURLFromQRCode() - { - Process.Start(_urlToOpen); - } - - private void QRCodeItem_Click(object sender, EventArgs e) - { - if (serverSharingWindow == null) - { - serverSharingWindow = new System.Windows.Window() - { - Title = LocalizationProvider.GetLocalizedValue("ServerSharing"), - Height = 400, - Width = 660, - MinHeight = 400, - MinWidth = 660, - Content = new ServerSharingView() - }; - serverSharingWindow.Closed += ServerSharingWindow_Closed; - ElementHost.EnableModelessKeyboardInterop(serverSharingWindow); - serverSharingWindow.Show(); - } - serverSharingWindow.Activate(); - } - - private void ServerSharingWindow_Closed(object sender, EventArgs e) - { - serverSharingWindow = null; - } - - private void ScanQRCodeItem_Click(object sender, EventArgs e) - { - var result = Utils.ScanQRCodeFromScreen(); - if (result != null) - { - if (result.ToLowerInvariant().StartsWith("http://") || result.ToLowerInvariant().StartsWith("https://")) - { - _urlToOpen = result; - openURLFromQRCode(); - } - else if (controller.AddServerBySSURL(result)) - { - ShowConfigForm(); - } - else - { - MessageBox.Show(I18N.GetString("Invalid QR Code content: {0}", result)); - } - return; - } - else - MessageBox.Show(I18N.GetString("No QRCode found. Try to zoom in or move it to the center of the screen.")); - } - - private void ImportURLItem_Click(object sender, EventArgs e) - { - if (controller.AskAddServerBySSURL(Clipboard.GetText(TextDataFormat.Text))) - { - ShowConfigForm(); - } - } - - #endregion - - #region PAC - - private void LocalPACItem_Click(object sender, EventArgs e) - { - if (!localPACItem.Checked) - { - localPACItem.Checked = true; - onlinePACItem.Checked = false; - controller.UseOnlinePAC(false); - UpdatePACItemsEnabledStatus(); - } - } - - private void OnlinePACItem_Click(object sender, EventArgs e) - { - if (!onlinePACItem.Checked) - { - if (string.IsNullOrEmpty(controller.GetCurrentConfiguration().pacUrl)) - { - UpdateOnlinePACURLItem_Click(sender, e); - } - if (!string.IsNullOrEmpty(controller.GetCurrentConfiguration().pacUrl)) - { - localPACItem.Checked = false; - onlinePACItem.Checked = true; - controller.UseOnlinePAC(true); - } - UpdatePACItemsEnabledStatus(); - } - } - - private void UpdateOnlinePACURLItem_Click(object sender, EventArgs e) - { - string origPacUrl = controller.GetCurrentConfiguration().pacUrl; - string pacUrl = Microsoft.VisualBasic.Interaction.InputBox( - I18N.GetString("Please input PAC Url"), - I18N.GetString("Edit Online PAC URL"), - origPacUrl, -1, -1); - if (!string.IsNullOrEmpty(pacUrl) && pacUrl != origPacUrl) - { - controller.SavePACUrl(pacUrl); - } - } - - private void SecureLocalPacUrlToggleItem_Click(object sender, EventArgs e) - { - Configuration configuration = controller.GetCurrentConfiguration(); - controller.ToggleSecureLocalPac(!configuration.secureLocalPac); - } - - private void RegenerateLocalPacOnUpdateItem_Click(object sender, EventArgs e) - { - var config = controller.GetCurrentConfiguration(); - controller.ToggleRegeneratePacOnUpdate(!config.regeneratePacOnUpdate); - } - - private void CopyLocalPacUrlItem_Click(object sender, EventArgs e) - { - controller.CopyPacUrl(); - } - - private void UpdatePACItemsEnabledStatus() - { - if (this.localPACItem.Checked) - { - this.editLocalPACItem.Enabled = true; - this.updateFromGeositeItem.Enabled = true; - this.editGFWUserRuleItem.Enabled = true; - this.editOnlinePACItem.Enabled = false; - } - else - { - this.editLocalPACItem.Enabled = false; - this.updateFromGeositeItem.Enabled = false; - this.editGFWUserRuleItem.Enabled = false; - this.editOnlinePACItem.Enabled = true; - } - } - - private void EditPACFileItem_Click(object sender, EventArgs e) - { - controller.TouchPACFile(); - } - - private async void UpdatePACFromGeositeItem_Click(object sender, EventArgs e) - { - await GeositeUpdater.UpdatePACFromGeosite(); - } - - private void EditUserRuleFileForGeositeItem_Click(object sender, EventArgs e) - { - controller.TouchUserRuleFile(); - } - - void controller_FileReadyToOpen(object sender, ShadowsocksController.PathEventArgs e) - { - string argument = @"/select, " + e.Path; - - Process.Start("explorer.exe", argument); - } - - void controller_UpdatePACFromGeositeError(object sender, System.IO.ErrorEventArgs e) - { - ShowBalloonTip(I18N.GetString("Failed to update PAC file"), e.GetException().Message, ToolTipIcon.Error, 5000); - logger.LogUsefulException(e.GetException()); - } - - void controller_UpdatePACFromGeositeCompleted(object sender, GeositeResultEventArgs e) - { - string result = e.Success - ? I18N.GetString("PAC updated") - : I18N.GetString("No updates found. Please report to Geosite if you have problems with it."); - ShowBalloonTip(I18N.GetString("Shadowsocks"), result, ToolTipIcon.Info, 1000); - } - - #endregion - - #region Help - - void controller_VerboseLoggingStatusChanged(object sender, EventArgs e) - { - VerboseLoggingToggleItem.Checked = controller.GetCurrentConfiguration().isVerboseLogging; - } - - void controller_ShowPluginOutputChanged(object sender, EventArgs e) - { - ShowPluginOutputToggleItem.Checked = controller.GetCurrentConfiguration().showPluginOutput; - } - - private void VerboseLoggingToggleItem_Click(object sender, EventArgs e) - { - VerboseLoggingToggleItem.Checked = !VerboseLoggingToggleItem.Checked; - controller.ToggleVerboseLogging(VerboseLoggingToggleItem.Checked); - } - - private void ShowLogItem_Click(object sender, EventArgs e) - { - ShowLogForm(); - } - - private void ShowPluginOutputToggleItem_Click(object sender, EventArgs e) - { - ShowPluginOutputToggleItem.Checked = !ShowPluginOutputToggleItem.Checked; - controller.ToggleShowPluginOutput(ShowPluginOutputToggleItem.Checked); - } - - private void WriteI18NFileItem_Click(object sender, EventArgs e) - { - File.WriteAllText(I18N.I18N_FILE, Resources.i18n_csv, Encoding.UTF8); - } - - #endregion - - #region Update - - void updateChecker_CheckUpdateCompleted(object sender, EventArgs e) - { - if (!_isStartupCheck && updateChecker.NewReleaseZipFilename == null) - { - ShowBalloonTip(I18N.GetString("Shadowsocks"), I18N.GetString("No update is available"), ToolTipIcon.Info, 5000); - } - _isStartupCheck = false; - } - - private void UpdateUpdateMenu() - { - Configuration configuration = controller.GetCurrentConfiguration(); - autoCheckUpdatesToggleItem.Checked = configuration.autoCheckUpdate; - checkPreReleaseToggleItem.Checked = configuration.checkPreRelease; - } - - private void autoCheckUpdatesToggleItem_Click(object sender, EventArgs e) - { - Configuration configuration = controller.GetCurrentConfiguration(); - controller.ToggleCheckingUpdate(!configuration.autoCheckUpdate); - UpdateUpdateMenu(); - } - - private void checkPreReleaseToggleItem_Click(object sender, EventArgs e) - { - Configuration configuration = controller.GetCurrentConfiguration(); - controller.ToggleCheckingPreRelease(!configuration.checkPreRelease); - UpdateUpdateMenu(); - } - - private async void checkUpdatesItem_Click(object sender, EventArgs e) - { - await updateChecker.CheckForVersionUpdate(); - } - - private void AboutItem_Click(object sender, EventArgs e) - { - Process.Start("https://github.com/shadowsocks/shadowsocks-windows"); - } - - #endregion - } -} diff --git a/shadowsocks-csharp/ViewModels/ForwardProxyViewModel.cs b/shadowsocks-csharp/ViewModels/ForwardProxyViewModel.cs deleted file mode 100644 index f901cac82..000000000 --- a/shadowsocks-csharp/ViewModels/ForwardProxyViewModel.cs +++ /dev/null @@ -1,131 +0,0 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using ReactiveUI.Validation.Extensions; -using ReactiveUI.Validation.Helpers; -using Shadowsocks.Controller; -using Shadowsocks.Model; -using Shadowsocks.View; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; - -namespace Shadowsocks.ViewModels -{ - public class ForwardProxyViewModel : ReactiveValidationObject - { - public ForwardProxyViewModel() - { - _config = Program.MainController.GetCurrentConfiguration(); - _controller = Program.MainController; - _menuViewController = Program.MenuController; - - if (!_config.proxy.useProxy) - NoProxy = true; - else if (_config.proxy.proxyType == 0) - UseSocks5Proxy = true; - else - UseHttpProxy = true; - - Address = _config.proxy.proxyServer; - Port = _config.proxy.proxyPort; - Timeout = _config.proxy.proxyTimeout; - - Username = _config.proxy.authUser; - Password = _config.proxy.authPwd; - - this.WhenAnyValue(x => x.NoProxy, x => !x) - .ToPropertyEx(this, x => x.CanModifyDetails); - - AddressRule = this.ValidationRule( - viewModel => viewModel.Address, - address => !string.IsNullOrWhiteSpace(address), - "Address can't be empty or whitespaces."); - PortRule = this.ValidationRule( - viewModel => viewModel.Port, - port => port > 0 && port <= 65535, - port => $"{port} is out of range (0, 65535]."); - TimeoutRule = this.ValidationRule( - viewModel => viewModel.Timeout, - timeout => timeout > 0 && timeout <= 10, - timeout => $"{timeout} is out of range (0, 10]."); - - var authValid = this - .WhenAnyValue(x => x.Username, x => x.Password, (username, password) => new { Username = username, Password = password }) - .Select(x => string.IsNullOrWhiteSpace(x.Username) == string.IsNullOrWhiteSpace(x.Password)); - AuthRule = this.ValidationRule(authValid, "You must provide both username and password."); - - var canSave = this.IsValid(); - - Save = ReactiveCommand.Create(() => - { - _controller.SaveProxy(GetForwardProxyConfig()); - _menuViewController.CloseForwardProxyWindow(); - }, canSave); - Cancel = ReactiveCommand.Create(_menuViewController.CloseForwardProxyWindow); - } - - private readonly Configuration _config; - private readonly ShadowsocksController _controller; - private readonly MenuViewController _menuViewController; - - public ValidationHelper AddressRule { get; } - public ValidationHelper PortRule { get; } - public ValidationHelper TimeoutRule { get; } - public ValidationHelper AuthRule { get; } - - public ReactiveCommand Save { get; } - public ReactiveCommand Cancel { get; } - - [ObservableAsProperty] - public bool CanModifyDetails { get; } - - [Reactive] - public bool NoProxy { get; set; } - - [Reactive] - public bool UseSocks5Proxy { get; set; } - - [Reactive] - public bool UseHttpProxy { get; set; } - - [Reactive] - public string Address { get; set; } - - [Reactive] - public int Port { get; set; } - - [Reactive] - public int Timeout { get; set; } - - [Reactive] - public string Username { get; set; } - - [Reactive] - public string Password { get; set; } - - private ForwardProxyConfig GetForwardProxyConfig() - { - var forwardProxyConfig = new ForwardProxyConfig() - { - proxyServer = Address, - proxyPort = Port, - proxyTimeout = Timeout, - authUser = Username, - authPwd = Password - }; - if (NoProxy) - forwardProxyConfig.useProxy = false; - else if (UseSocks5Proxy) - { - forwardProxyConfig.useProxy = true; - forwardProxyConfig.proxyType = 0; - } - else - { - forwardProxyConfig.useProxy = true; - forwardProxyConfig.proxyType = 1; - } - return forwardProxyConfig; - } - } -} diff --git a/shadowsocks-csharp/ViewModels/HotkeysViewModel.cs b/shadowsocks-csharp/ViewModels/HotkeysViewModel.cs deleted file mode 100644 index 141d2cf92..000000000 --- a/shadowsocks-csharp/ViewModels/HotkeysViewModel.cs +++ /dev/null @@ -1,189 +0,0 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Shadowsocks.Controller; -using Shadowsocks.Model; -using Shadowsocks.View; -using System.Reactive; -using System.Text; -using System.Windows.Input; - -namespace Shadowsocks.ViewModels -{ - public class HotkeysViewModel : ReactiveObject - { - public HotkeysViewModel() - { - _config = Program.MainController.GetCurrentConfiguration(); - _controller = Program.MainController; - _menuViewController = Program.MenuController; - - HotkeySystemProxy = _config.hotkey.SwitchSystemProxy; - HotkeyProxyMode = _config.hotkey.SwitchSystemProxyMode; - HotkeyAllowLan = _config.hotkey.SwitchAllowLan; - HotkeyOpenLogs = _config.hotkey.ShowLogs; - HotkeySwitchPrev = _config.hotkey.ServerMoveUp; - HotkeySwitchNext = _config.hotkey.ServerMoveDown; - RegisterAtStartup = _config.hotkey.RegHotkeysAtStartup; - - HotkeySystemProxyStatus = "✔"; - HotkeyProxyModeStatus = "✔"; - HotkeyAllowLanStatus = "✔"; - HotkeyOpenLogsStatus = "✔"; - HotkeySwitchPrevStatus = "✔"; - HotkeySwitchNextStatus = "✔"; - - RegisterAll = ReactiveCommand.Create(() => RegisterAllAndUpdateStatus()); - Save = ReactiveCommand.Create(() => RegisterAllAndUpdateStatus(true)); - Cancel = ReactiveCommand.Create(_menuViewController.CloseHotkeysWindow); - } - - private readonly Configuration _config; - private readonly ShadowsocksController _controller; - private readonly MenuViewController _menuViewController; - - public ReactiveCommand RegisterAll { get; } - public ReactiveCommand Save { get; } - public ReactiveCommand Cancel { get; } - - [Reactive] - public string HotkeySystemProxy { get; set; } - - [Reactive] - public string HotkeyProxyMode { get; set; } - - [Reactive] - public string HotkeyAllowLan { get; set; } - - [Reactive] - public string HotkeyOpenLogs { get; set; } - - [Reactive] - public string HotkeySwitchPrev { get; set; } - - [Reactive] - public string HotkeySwitchNext { get; set; } - - [Reactive] - public bool RegisterAtStartup { get; set; } - - [Reactive] - public string HotkeySystemProxyStatus { get; set; } - - [Reactive] - public string HotkeyProxyModeStatus { get; set; } - - [Reactive] - public string HotkeyAllowLanStatus { get; set; } - - [Reactive] - public string HotkeyOpenLogsStatus { get; set; } - - [Reactive] - public string HotkeySwitchPrevStatus { get; set; } - - [Reactive] - public string HotkeySwitchNextStatus { get; set; } - - public void RecordKeyDown(int hotkeyIndex, KeyEventArgs keyEventArgs) - { - var recordedKeyStringBuilder = new StringBuilder(); - - // record modifiers - if ((Keyboard.Modifiers & ModifierKeys.Control) > 0) - recordedKeyStringBuilder.Append("Ctrl+"); - if ((Keyboard.Modifiers & ModifierKeys.Alt) > 0) - recordedKeyStringBuilder.Append("Alt+"); - if ((Keyboard.Modifiers & ModifierKeys.Shift) > 0) - recordedKeyStringBuilder.Append("Shift+"); - - // record other keys when at least one modifier is pressed - if (recordedKeyStringBuilder.Length > 0 && (keyEventArgs.Key < Key.LeftShift || keyEventArgs.Key > Key.RightAlt)) - recordedKeyStringBuilder.Append(keyEventArgs.Key); - - switch (hotkeyIndex) - { - case 0: - HotkeySystemProxy = recordedKeyStringBuilder.ToString(); - break; - case 1: - HotkeyProxyMode = recordedKeyStringBuilder.ToString(); - break; - case 2: - HotkeyAllowLan = recordedKeyStringBuilder.ToString(); - break; - case 3: - HotkeyOpenLogs = recordedKeyStringBuilder.ToString(); - break; - case 4: - HotkeySwitchPrev = recordedKeyStringBuilder.ToString(); - break; - case 5: - HotkeySwitchNext = recordedKeyStringBuilder.ToString(); - break; - } - } - - public void FinishOnKeyUp(int hotkeyIndex, KeyEventArgs keyEventArgs) - { - switch (hotkeyIndex) - { - case 0: - if (HotkeySystemProxy.EndsWith("+")) - HotkeySystemProxy = ""; - break; - case 1: - if (HotkeyProxyMode.EndsWith("+")) - HotkeyProxyMode = ""; - break; - case 2: - if (HotkeyAllowLan.EndsWith("+")) - HotkeyAllowLan = ""; - break; - case 3: - if (HotkeyOpenLogs.EndsWith("+")) - HotkeyOpenLogs = ""; - break; - case 4: - if (HotkeySwitchPrev.EndsWith("+")) - HotkeySwitchPrev = ""; - break; - case 5: - if (HotkeySwitchNext.EndsWith("+")) - HotkeySwitchNext = ""; - break; - } - } - - private void RegisterAllAndUpdateStatus(bool save = false) - { - HotkeySystemProxyStatus = HotkeyReg.RegHotkeyFromString(HotkeySystemProxy, "SwitchSystemProxyCallback") ? "✔" : "❌"; - HotkeyProxyModeStatus = HotkeyReg.RegHotkeyFromString(HotkeyProxyMode, "SwitchSystemProxyModeCallback") ? "✔" : "❌"; - HotkeyAllowLanStatus = HotkeyReg.RegHotkeyFromString(HotkeyAllowLan, "SwitchAllowLanCallback") ? "✔" : "❌"; - HotkeyOpenLogsStatus = HotkeyReg.RegHotkeyFromString(HotkeyOpenLogs, "ShowLogsCallback") ? "✔" : "❌"; - HotkeySwitchPrevStatus = HotkeyReg.RegHotkeyFromString(HotkeySwitchPrev, "ServerMoveUpCallback") ? "✔" : "❌"; - HotkeySwitchNextStatus = HotkeyReg.RegHotkeyFromString(HotkeySwitchNext, "ServerMoveDownCallback") ? "✔" : "❌"; - - if (HotkeySystemProxyStatus == "✔" && - HotkeyProxyModeStatus == "✔" && - HotkeyAllowLanStatus == "✔" && - HotkeyOpenLogsStatus == "✔" && - HotkeySwitchPrevStatus == "✔" && - HotkeySwitchNextStatus == "✔" && save) - { - _controller.SaveHotkeyConfig(GetHotkeyConfig); - _menuViewController.CloseHotkeysWindow(); - } - } - - private HotkeyConfig GetHotkeyConfig => new HotkeyConfig() - { - SwitchSystemProxy = HotkeySystemProxy, - SwitchSystemProxyMode = HotkeyProxyMode, - SwitchAllowLan = HotkeyAllowLan, - ShowLogs = HotkeyOpenLogs, - ServerMoveUp = HotkeySwitchPrev, - ServerMoveDown = HotkeySwitchNext, - RegHotkeysAtStartup = RegisterAtStartup - }; - } -} diff --git a/shadowsocks-csharp/ViewModels/OnlineConfigViewModel.cs b/shadowsocks-csharp/ViewModels/OnlineConfigViewModel.cs deleted file mode 100644 index f803b3514..000000000 --- a/shadowsocks-csharp/ViewModels/OnlineConfigViewModel.cs +++ /dev/null @@ -1,115 +0,0 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using ReactiveUI.Validation.Extensions; -using ReactiveUI.Validation.Helpers; -using Shadowsocks.Controller; -using Shadowsocks.Localization; -using Shadowsocks.Model; -using Shadowsocks.View; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using System.Text; -using System.Windows; - -namespace Shadowsocks.ViewModels -{ - public class OnlineConfigViewModel : ReactiveValidationObject - { - public OnlineConfigViewModel() - { - _config = Program.MainController.GetCurrentConfiguration(); - _controller = Program.MainController; - _menuViewController = Program.MenuController; - - Sources = new ObservableCollection(_config.onlineConfigSource); - SelectedSource = ""; - Address = ""; - - // TODO in v5: if http:// show warning as materialDesign:HintAssist.HelperText - AddressRule = this.ValidationRule( - viewModel => viewModel.Address, - address => address.StartsWith("http://"), - "Warning: getting online configuration from plain HTTP sources is NOT secure!"); - - var canUpdateCopyRemove = this.WhenAnyValue( - x => x.SelectedSource, - selectedSource => !string.IsNullOrWhiteSpace(selectedSource)); - var canUpdateAll = this.WhenAnyValue( - x => x.Sources.Count, - count => count > 0); - var canAdd = this.WhenAnyValue( - x => x.Address, - address => Uri.IsWellFormedUriString(address, UriKind.Absolute) && - (address.StartsWith("https://") || address.StartsWith("http://"))); - - Update = ReactiveCommand.CreateFromTask(() => _controller.UpdateOnlineConfig(SelectedSource), canUpdateCopyRemove); - UpdateAll = ReactiveCommand.CreateFromTask(_controller.UpdateAllOnlineConfig, canUpdateAll); - CopyLink = ReactiveCommand.Create(() => Clipboard.SetText(SelectedSource), canUpdateCopyRemove); - Remove = ReactiveCommand.Create(() => - { - bool result; - var urlToRemove = SelectedSource; // save it here because SelectedSource is lost once we remove the selection - do - { - result = Sources.Remove(urlToRemove); - } while (result); - _controller.RemoveOnlineConfig(urlToRemove); - }, canUpdateCopyRemove); - Add = ReactiveCommand.Create(() => - { - Sources.Add(Address); - SelectedSource = Address; - _controller.SaveOnlineConfigSource(Sources.ToList()); - Address = ""; - }, canAdd); - - // TODO in v5: use MaterialDesignThemes snackbar messages - this.WhenAnyObservable(x => x.Update) - .Subscribe(x => - { - if (x) - MessageBox.Show(LocalizationProvider.GetLocalizedValue("sip008UpdateSuccess")); - else - MessageBox.Show(LocalizationProvider.GetLocalizedValue("sip008UpdateFailure")); - }); - this.WhenAnyObservable(x => x.UpdateAll) - .Subscribe(x => - { - if (x.Count == 0) - MessageBox.Show(LocalizationProvider.GetLocalizedValue("sip008UpdateAllSuccess")); - else - { - var stringBuilder = new StringBuilder(LocalizationProvider.GetLocalizedValue("sip008UpdateAllFailure")); - foreach (var url in x) - stringBuilder.AppendLine(url); - MessageBox.Show(stringBuilder.ToString()); - } - }); - } - - private readonly Configuration _config; - private readonly ShadowsocksController _controller; - private readonly MenuViewController _menuViewController; - - public ValidationHelper AddressRule { get; } - - public ReactiveCommand Update { get; } - public ReactiveCommand> UpdateAll { get; } - public ReactiveCommand CopyLink { get; } - public ReactiveCommand Remove { get; } - public ReactiveCommand Add { get; } - - [Reactive] - public ObservableCollection Sources { get; private set; } - - [Reactive] - public string SelectedSource { get; set; } - - [Reactive] - public string Address { get; set; } - } -} diff --git a/shadowsocks-csharp/ViewModels/ServerSharingViewModel.cs b/shadowsocks-csharp/ViewModels/ServerSharingViewModel.cs deleted file mode 100644 index f13756908..000000000 --- a/shadowsocks-csharp/ViewModels/ServerSharingViewModel.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Shadowsocks.Model; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Reactive; -using System.Windows; -using System.Windows.Media.Imaging; - -namespace Shadowsocks.ViewModels -{ - public class ServerSharingViewModel : ReactiveObject - { - /// - /// The view model class for the server sharing user control. - /// - public ServerSharingViewModel() - { - _config = Program.MainController.GetCurrentConfiguration(); - Servers = _config.configs; - SelectedServer = Servers[0]; - - this.WhenAnyValue(x => x.SelectedServer) - .Subscribe(_ => UpdateUrlAndImage()); - - CopyLink = ReactiveCommand.Create(() => Clipboard.SetText(SelectedServerUrl)); - } - - private readonly Configuration _config; - - public ReactiveCommand CopyLink { get; } - - [Reactive] - public List Servers { get; private set; } - - [Reactive] - public Server SelectedServer { get; set; } - - [Reactive] - public string SelectedServerUrl { get; private set; } - - [Reactive] - public BitmapImage SelectedServerUrlImage { get; private set; } - - /// - /// Called when SelectedServer changed - /// to update SelectedServerUrl and SelectedServerUrlImage - /// - private void UpdateUrlAndImage() - { - // update SelectedServerUrl - SelectedServerUrl = SelectedServer.GetURL(_config.generateLegacyUrl); - - // generate QR code - var qrCode = ZXing.QrCode.Internal.Encoder.encode(SelectedServerUrl, ZXing.QrCode.Internal.ErrorCorrectionLevel.L); - var byteMatrix = qrCode.Matrix; - - // paint bitmap - int blockSize = Math.Max(1024 / byteMatrix.Height, 1); - Bitmap drawArea = new Bitmap((byteMatrix.Width * blockSize), (byteMatrix.Height * blockSize)); - using (var graphics = Graphics.FromImage(drawArea)) - { - graphics.Clear(Color.White); - using (var solidBrush = new SolidBrush(Color.Black)) - { - for (int row = 0; row < byteMatrix.Width; row++) - { - for (int column = 0; column < byteMatrix.Height; column++) - { - if (byteMatrix[row, column] != 0) - { - graphics.FillRectangle(solidBrush, blockSize * row, blockSize * column, blockSize, blockSize); - } - } - } - } - } - - // transform to BitmapImage for binding - BitmapImage bitmapImage = new BitmapImage(); - using (MemoryStream memoryStream = new MemoryStream()) - { - drawArea.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp); - memoryStream.Position = 0; - bitmapImage.BeginInit(); - bitmapImage.StreamSource = memoryStream; - bitmapImage.CacheOption = BitmapCacheOption.OnLoad; - bitmapImage.EndInit(); - } - SelectedServerUrlImage = bitmapImage; - } - } -} diff --git a/shadowsocks-csharp/ViewModels/VersionUpdatePromptViewModel.cs b/shadowsocks-csharp/ViewModels/VersionUpdatePromptViewModel.cs deleted file mode 100644 index 0ae9f2d17..000000000 --- a/shadowsocks-csharp/ViewModels/VersionUpdatePromptViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Newtonsoft.Json.Linq; -using ReactiveUI; -using Shadowsocks.Controller; -using System.Reactive; - -namespace Shadowsocks.ViewModels -{ - public class VersionUpdatePromptViewModel : ReactiveObject - { - public VersionUpdatePromptViewModel(JToken releaseObject) - { - _updateChecker = Program.MenuController.updateChecker; - _releaseObject = releaseObject; - ReleaseNotes = string.Concat( - $"# {((bool)_releaseObject["prerelease"] ? "⚠ Pre-release" : "ℹ Release")} {(string)_releaseObject["tag_name"] ?? "Failed to get tag name"}\r\n", - (string)_releaseObject["body"] ?? "Failed to get release notes"); - - Update = ReactiveCommand.CreateFromTask(_updateChecker.DoUpdate); - SkipVersion = ReactiveCommand.Create(_updateChecker.SkipUpdate); - NotNow = ReactiveCommand.Create(_updateChecker.CloseVersionUpdatePromptWindow); - } - - private readonly UpdateChecker _updateChecker; - private readonly JToken _releaseObject; - - public string ReleaseNotes { get; } - - public ReactiveCommand Update { get; } - - public ReactiveCommand SkipVersion { get; } - - public ReactiveCommand NotNow { get; } - } -} diff --git a/shadowsocks-csharp/Views/ForwardProxyView.xaml b/shadowsocks-csharp/Views/ForwardProxyView.xaml deleted file mode 100644 index 0c6a9319d..000000000 --- a/shadowsocks-csharp/Views/ForwardProxyView.xaml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -