Skip to content

EN Architecture

SlimRG edited this page Aug 23, 2026 · 1 revision

Architecture

shadowsocks-reborn 5.x is a WinUI 3 desktop application split into platform-neutral core logic, Windows integration, WinUI integration, the product shell, and an isolated elevated capture helper.

Project graph

Shadowsocks.WinUI
  ├── Shadowsocks.Core
  ├── Shadowsocks.Windows
  └── Shadowsocks.Windows.WinUI

Shadowsocks.Windows
  └── Shadowsocks.Core

Shadowsocks.Windows.WinUI
  └── standalone WinUI shell-integration library (no project references)

Shadowsocks.NetworkService
  └── isolated elevated process / named-pipe boundary
      + linked dependency-free FilterEngine.cs source

Projects:

  • Shadowsocks.Core (net10.0) — protocol, encryption, configuration model, PAC/GeoSite logic, localization, routing models, logging configuration and storage abstractions.
  • Shadowsocks.Windows — file-storage bootstrap, WinINet/system proxy, startup, self-update, hotkeys, SIP003 package management/process hosting, UAC/Admin capture, WinDivert runtime and NetworkService coordination.
  • Shadowsocks.Windows.WinUI — standalone WinUI-specific shell/tray/QR/power integration; it intentionally has no Core or Windows project dependency.
  • Shadowsocks.WinUI — unpackaged WinUI 3 application and product publish project. Release assembly name is Shadowsocks.
  • Shadowsocks.NetworkService — isolated elevated x64 helper that owns transparent WinDivert capture/routing.
  • Shadowsocks.UnitTests — Core/Windows tests without presentation dependencies.

Shadowsocks.Core must remain free of WinUI, WinForms, WPF and Windows App SDK dependencies. Shadowsocks.NetworkService is deliberately not a normal executable ProjectReference of the product.

Managed ABP/EasyList routing — final architecture

The migration is complete. The dependency-free Shadowsocks.Routing.FilterEngine is the only Local GeoSite/EasyList/ABP routing authority. Immutable ManagedRoutingSnapshot instances are published to the live managed HTTP path and the same rule set is supplied to the elevated Administrator transport. The historical abp.js runtime, the compiled-PAC compatibility backend, the historical backend selector, and the executable custom abp.txt override have been removed. Settings schema migration canonicalizes the typed configuration, so removed properties are pruned from existing JSON.

Current managed-filter contract:

  • ordinary matching rules produce PROXY; @@ exception rules produce DIRECT; no match produces DIRECT;
  • user rules are evaluated before generated/default GeoSite rules, and exceptions win over blocking matches within a rule set;
  • || domain anchors, | start/end anchors, *, ^, explicit regular expressions, $domain= and $match-case are implemented in managed code;
  • simple ||domain^ rules use a hostname suffix index without per-rule Regex allocation; complex rules use the ABP keyword index and a bounded decision cache;
  • malformed/unsupported rules are reported without replacing the last known-good live snapshot;
  • browser cosmetic filtering remains out of scope.

User Mode / Local PAC

For the normal Local PAC path, PAC is now intentionally tiny:

WinINet / system PAC client
  ↓
minimal FindProxyForURL funnel
  ↓
ManagedHttpProxyService
  ↓ application rule
TrafficPolicyEngine
  ↓ managed GeoSite + user-rule snapshot
DIRECT or PROXY

This removes the historical built-in abp.js matcher from the normal Local PAC hot path. GeoSite updates still provide the managed rule data, but they no longer regenerate a large JavaScript matcher in Managed mode. Snapshot rebuilds are atomic and are triggered by configuration, GeoSite and rule-file changes.

The PAC / GeoSite page no longer exposes a Local routing backend selector because Local routing is always managed C#. The UI reports the effective mode, snapshot generation/source, default/user/invalid rule counts, local DIRECT/PROXY counters, and Admin-helper managed-routing state. These counters are process-local diagnostics only; URLs/domains are not transmitted as telemetry. The Local PAC file itself is deliberately tiny: FindProxyForURL always returns the local proxy endpoint so the C# policy engine receives the request and makes the routing decision.

No Local ABP/EasyList JavaScript matcher remains. The only JavaScript-related feature is Online PAC, which is a separate explicit mode that executes the user-selected external PAC program; arbitrary PAC programs cannot be losslessly translated into ABP/EasyList rules. This does not affect Local GeoSite/EasyList/ABP routing, which stays entirely in C#.

Administrator Mode / late TCP decision

WinDivert's SYN-time NETWORK decision has process identity and destination IP but not a trustworthy domain name. The final design therefore does not guess IP→domain. For ordinary TCP that has no explicit application rule, NetworkService returns a Deferred action and reflects the connection into its local transparent TCP relay.

The relay buffers only the initial client bytes (bounded to 64 KiB) and looks for clear-text routing metadata:

  • HTTP request line + Host header → hostname plus full HTTP path/query context;
  • TLS ClientHello SNI → hostname only.

No TLS payload is decrypted, no CA is installed and no certificate is generated or replaced. After Host/SNI inspection, the helper runs the same dependency-free FilterEngine source that Core uses and opens either a direct socket to the original destination or the existing Shadowsocks SOCKS5 path. The buffered prefix is forwarded unchanged.

Explicit application Direct/Proxy/Block rules remain higher priority and do not require inspection. TCP DNS/53 retains the DNS policy path. UDP has no reliable hostname at this layer, and ECH/no-SNI/server-first/non-HTTP protocols use the deterministic fallback; the relay may wait briefly for initial client metadata before falling back. HTTPS path rules cannot be evaluated from SNI without MITM, so TLS routing is intentionally hostname-only.

The elevated helper compiles FilterEngine.cs as linked dependency-free source under its own namespace rather than referencing the full Core assembly. Managed rule sets travel over the existing named-pipe protocol, not process command lines. GeoSite/user-rule/custom-backend changes coalesce while an Admin capture-child restart is in flight; if the broker is already elevated, refreshing the capture child does not create another UAC prompt.

Application lifecycle

WinUI 3 is the only desktop presentation layer.

Startup flow:

Program.Main
  ↓
Windows App SDK AppInstance
  ↓ same-identity activation redirect
ProcessSingleInstanceGuard
  ↓ executable-identity-independent process gate
App
  ↓
WindowsStorageBootstrapper
  ↓
ShadowsocksController
  ↓
MainWindow / tray / pages

The application uses a stable AppInstance key for normal same-identity activation redirection and a separate named ProcessSingleInstanceGuard before controller startup. The second gate is intentionally independent of unpackaged WinUI executable identity, so the LocalAppData startup copy and an original/USB copy cannot both become controller owners. Same-identity user launches still redirect to the existing WinUI instance; a cross-identity duplicate exits before networking/controller startup.

Closing the main window hides it; the tray keeps the application alive. Explicit Quit performs controller shutdown and runtime cleanup.

Presentation boundary

Pages read state and perform operations through controller/services. They do not own a second networking/configuration model; persistence and Registry integration stay behind storage/Windows services except for simple shell actions such as opening the data folder.

WinUI-specific code belongs in Shadowsocks.WinUI or Shadowsocks.Windows.WinUI. Synchronous Windows integration belongs behind services in Shadowsocks.Windows; do not reintroduce WinForms/WPF to solve shell/platform tasks.

Traffic architecture

WinUI / tray
  ↓
ShadowsocksController
  ↓
GameModeManager / AdminCaptureManager
  ↓ named pipe
Shadowsocks.NetworkService (elevated)
  ↓
WinDivert

The Admin capture path opens a read-only WinDivert FLOW observer before the NETWORK packet handle. FLOW events populate endpoint-to-PID ownership used by routing; IP Helper tables remain a fallback for flows that pre-date the observer or are ambiguous. WinDivert 2.2.2 x64 runtime files are accepted only when their pinned SHA-256 values match. Fragmented datagrams that require transparent rewriting are dropped as a complete flow rather than allowing later fragments to bypass the first-fragment decision.

Only User and Admin are selectable traffic modes.

  • User Mode does not require elevation or NetworkService extraction.
  • Admin Mode acquires an on-demand NetworkService runtime lease, launches the broker elevated, validates its protocol version, and enables transparent TCP/UDP capture.
  • Game Mode is automatic: a configured running application suspends Admin capture while keeping Admin as the configured mode. Capture restores automatically when the trigger exits.

Application rules support Proxy, Direct and Block.

SIP003 plugin update boundary

PluginManager owns catalog install/update, manual archive import, provenance metadata and executable resolution. Only the trusted built-in catalog (xray-plugin, v2ray-plugin, qtun) is eligible for background network updates. Controller maintenance starts after application startup, performs an hourly lightweight maintenance tick, and the manager gates successful release checks to once per 24 hours. In-use plugins are deferred; manual archives never auto-update. Package promotion uses staging plus rollback-safe directory replacement and revalidates trust immediately before swap. An arbitrary repository source is not accepted until a separate trust and release-asset-selection contract exists.

Storage architecture

Core uses ISettingsStore; product startup configures JsonFileSettingsStore.

Normal mode:

%LOCALAPPDATA%\Shadowsocks
  settings.json
  settings.backup.json
  Cache\...
  Data\...
  Plugins\...
  Logs\...
  Runtime\...

Clean Mode is selected by an executable stem ending in p and redirects the same logical tree to:

%TEMP%\Shadowsocks\Clean\<session>\...

The Registry is not an application-configuration backend. Registry access remains only for Windows integrations that require it (Run, protocol association, WinINet/system proxy, discovery reads).

The release directory is immutable application code. Product runtime does not use executable-side files as a configuration, migration, plugin, cache, log or update source.

See STORAGE_POLICY.md.

PAC, GeoSite, localization and logging

Default application resources are embedded. Mutable state is externalized:

  • PAC/user data → %LOCALAPPDATA%\Shadowsocks\Data\PAC;
  • Online PAC cache → %LOCALAPPDATA%\Shadowsocks\Cache\PAC;
  • GeoSite cache/runtime → %LOCALAPPDATA%\Shadowsocks\Cache\GeoSite and Runtime\WinDivert as applicable;
  • SIP003 plugin packages → %LOCALAPPDATA%\Shadowsocks\Plugins (or the active Clean Mode storage root);
  • localization → embedded Shadowsocks.Core.Data.i18n.csv only;
  • logs → %LOCALAPPDATA%\Shadowsocks\Logs.

NLog is configured programmatically; product deployment does not require NLog.config.

Startup copy

When Start with Windows is enabled, AutoStartup copies the current executable to:

%LOCALAPPDATA%\Shadowsocks\Startup\Shadowsocks.exe

The copy is SHA-256 checked and the HKCU Run entry points to that stable path with --start-hidden. A legacy-only matching Run entry is migrated to the canonical entry; stale matching values are removed both while autostart is enabled and when it is disabled. A process-wide named gate also prevents concurrent startup-copy/original-EXE controller startup. Start with Windows is disabled in Clean Mode.

Application self-update

Application updates use the canonical GitHub assets Shadowsocks-win-x64.zip and Shadowsocks-win-x64.zip.sha256. The updater rejects missing/non-canonical assets, invalid GitHub release URLs, checksum mismatches, ZIPs that do not contain exactly one root Shadowsocks.exe, and payloads whose file version does not match the selected release.

The single-file handoff is:

  1. download and verify the release into %TEMP%\Shadowsocks\Updates\<transaction>;
  2. extract the new payload as Shadowsocks.Update.exe, calculate its SHA-256 and retain a read-only/non-deletable handle across process creation/UAC;
  3. start that payload with the internal --update handoff plus the expected updater digest, and only then shut down the current instance;
  4. the temporary payload re-verifies its own staged image against the handoff digest before replacement, waits for the old PID, keeps a rollback copy, replaces the product EXE and starts the installed new copy;
  5. the installed new copy waits for the temporary updater to exit and removes the transaction/rollback files.

If the running process is the stable Start-with-Windows copy, its Run command carries the original product EXE path via an internal startup-origin argument. Update eligibility uses the higher of the running copy version and the primary EXE FileVersion, so an out-of-date startup copy cannot downgrade a newer primary executable. Updater mode rechecks the target FileVersion immediately before replacement and accepts only a strictly newer payload. Updates therefore replace the user-facing product binary rather than only the LocalAppData startup copy. Clean Mode executables are updated under their current ...p.exe name while the self-update transaction itself stays outside the disposable Clean Mode session. Release packaging additionally verifies that the published Shadowsocks.exe FileVersion exactly matches the requested release version before ZIP creation.

Embedded NetworkService

Product publish uses a separate build step for the elevated helper:

  1. publish Shadowsocks.NetworkService as self-contained, win-x64 and single-file;
  2. embed the helper EXE as Shadowsocks.WinUI.Embedded.Shadowsocks.NetworkService.exe;
  3. bundle that resource into the final Shadowsocks.exe;
  4. reject every release sidecar.

Admin runtime materialization:

  1. hash embedded helper bytes;
  2. serialize extraction with a named mutex;
  3. create a unique Temp\NetworkService\<version>\<hash>\<pid-guid> directory below the active storage root;
  4. write and verify the helper SHA-256;
  5. launch with UAC;
  6. verify helper version over the control-pipe ping handshake;
  7. keep a guard handle for the broker lifetime;
  8. remove the current run directory on shutdown and prune stale directories later.

User Mode does not extract NetworkService.

Product deployment

Release profile:

unpackaged
self-contained
win-x64
PublishSingleFile
WindowsAppSDKSelfContained

Distribution invariant:

Release/
└── Shadowsocks.exe

CI, packaging/Build-Release.ps1 and packaging/Validate-Repository.ps1 enforce the project boundaries and one-file release layout.

Release validation philosophy

Directory.Build.props is the single source for the three-part product version. Materialized four-part versions in ApplicationInfo and application manifests are checked against it.

Validate-Repository.ps1 deliberately validates structure and release invariants, not implementation spelling: project graph, target frameworks, packaging profile, embedded resources, localization completeness, encoding/line endings, absence of retired JavaScript/build output, PowerShell parser validity and security/supply-chain invariants. Behavioral contracts belong in Shadowsocks.UnitTests and are executed by Build-Release.ps1. This keeps normal refactoring from failing merely because a source token moved or was renamed.

DNSCrypt resolver-catalog bootstrap

Shadowsocks Reborn refreshes the signed DNSCrypt resolver catalog itself instead of allowing dnscrypt-proxy to bootstrap remote source hostnames. Cloudflare DoH (1.1.1.1, TLS/SNI cloudflare-dns.com) is the primary resolver and Google DoH (8.8.8.8, TLS/SNI dns.google) is the fallback; both DoH HTTPS connections are opened through the active local Shadowsocks SOCKS5 listener. The HTTPS download of public-resolvers.md and .minisig also travels through Shadowsocks after the source hostname is resolved by that DoH path.

The downloaded pair is size-bounded and verified with the pinned DNSCrypt public Minisign key before publication. dnscrypt-proxy then receives only the local authenticated cache with urls = [], bootstrap_resolvers = [] and ignore_system_dns = true. If refresh is temporarily unavailable, an existing valid signed cache may continue to be used; without a valid cache the operation fails closed and the previous DNS mode is retained.

Clone this wiki locally