How the Shadowsocks Windows Configuration System Loads, Saves, and Processes Changes

The Shadowsocks Windows configuration system uses three static methods—Load(), Process(), and Save()—in shadowsocks-csharp/Model/Configuration.cs to deserialize gui-config.json into a Configuration object, validate and enrich it with runtime defaults (geosite groups, server indices, IPv6 checks), and persist changes back to disk while synchronizing NLog logging levels.

The Configuration class in the shadowsocks/shadowsocks-windows repository manages the complete lifecycle of user settings through a structured three-phase pipeline. This system handles everything from initial file I/O and JSON deserialization to runtime validation and persistent storage, ensuring that proxy settings, server lists, and UI preferences remain consistent across application sessions.

Loading the Configuration from Disk

The ** Load() ** method (lines 61‑74) serves as the entry point for reading persisted settings. It first checks for the existence of gui-config.json using File.Exists(CONFIG_FILE). If the file is present, the method reads its contents and deserializes the JSON using Newtonsoft.Json with ObjectCreationHandling.Replace to ensure collections are recreated rather than appended to existing objects.

If the file is missing or contains malformed JSON, the method falls back to instantiating a fresh Configuration object with sensible defaults defined in the constructor. This defensive approach ensures the application always boots with a valid configuration state.

public static Configuration Load()
{
    if (File.Exists(CONFIG_FILE))
    {
        try
        {
            string configContent = File.ReadAllText(CONFIG_FILE);
            return JsonConvert.DeserializeObject<Configuration>(configContent,
                new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace });
        }
        catch (Exception e) { logger.LogUsefulException(e); }
    }
    return new Configuration();          // defaults
}

Processing and Validating Configuration Data

After loading, the ** Process(ref Configuration) ** method (lines 85‑124) performs a comprehensive validation and enrichment pass. This method is called exactly once during application startup to normalize data and adapt settings to the current runtime environment.

Geosite Group Validation

The system validates geosite categorization lists for both direct and proxied connections using ValidateGeositeGroupList(). If any configured group is unknown or invalid, the method resets the respective list to safe defaults via ResetGeositeDirectGroup() or ResetGeositeProxiedGroup().

Version Migration and Server List Management

The method detects application upgrades by comparing UpdateChecker.Version against the stored config.version. When the application version is newer, it sets the firstRunOnNewVersion flag to trigger welcome flows or migration logic. The processor also ensures the server list is never empty by injecting a GetDefaultServer() when configs.Count == 0, and it corrects the index pointer to ensure it points to a valid server or defaults to 0.

System Capability and Proxy Checks

Runtime environment validation includes disabling isIPv6Enabled automatically when System.Net.Sockets.Socket.OSSupportsIPv6 returns false. The method invokes config.proxy.CheckConfig() to validate forward-proxy settings and interpolates the user-agent string by replacing the $version placeholder with the actual version number.

NLog Integration

The processing phase synchronizes logging verbosity by loading the NLog XML configuration via NLogConfig.LoadXML(). It inspects the current log level and sets the isVerboseLogging boolean to true if the level is Debug or Trace, enabling conditional verbose output throughout the application.

public static void Process(ref Configuration config)
{
    // Geosite validation
    if (!ValidateGeositeGroupList(config.geositeDirectGroups))
        ResetGeositeDirectGroup(ref config.geositeDirectGroups);
    if (!ValidateGeositeGroupList(config.geositeProxiedGroups))
        ResetGeositeProxiedGroup(ref config.geositeProxiedGroups);

    // Version detection
    var appVersion = new Version(UpdateChecker.Version);
    var cfgVersion = new Version(config.version);
    if (appVersion.CompareTo(cfgVersion) > 0) config.firstRunOnNewVersion = true;

    // Server list sanity
    if (config.configs.Count == 0) config.configs.Add(GetDefaultServer());
    if (config.index == -1 && string.IsNullOrEmpty(config.strategy)) config.index = 0;
    if (config.index >= config.configs.Count) config.index = config.configs.Count - 1;

    // System capability checks
    if (!System.Net.Sockets.Socket.OSSupportsIPv6) config.isIPv6Enabled = false;
    config.proxy.CheckConfig();
    config.userAgentString = config.userAgent.Replace("$version", config.version);

    // NLog level detection
    try
    {
        config.nLogConfig = NLogConfig.LoadXML();
        config.isVerboseLogging = config.nLogConfig.GetLogLevel()
            is NLogConfig.LogLevel.Debug or NLogConfig.LogLevel.Trace;
    }
    catch (Exception e) { /* error handling */ }
}

Saving Configuration Changes

The ** Save(Configuration) ** method (lines 136‑166) handles persistence when the application shuts down or the user explicitly triggers a save. Before writing, it reorders the server list using SortByOnlineConfig() to ensure ungrouped servers appear first, maintaining a deterministic order for online configuration sources.

The method opens gui-config.json with FileMode.Create, effectively truncating and rewriting the file atomically. It serializes the configuration using Formatting.Indented for human readability. Finally, it synchronizes the NLog log level based on isVerboseLogging and persists the XML configuration back to disk, ensuring logging settings survive application restarts.

public static void Save(Configuration config)
{
    config.configs = SortByOnlineConfig(config.configs);

    try
    {
        using var fs = File.Open(CONFIG_FILE, FileMode.Create);
        using var writer = new StreamWriter(fs);
        writer.Write(JsonConvert.SerializeObject(config, Formatting.Indented));

        config.nLogConfig.SetLogLevel(
            config.isVerboseLogging ? verboseLogLevel : NLogConfig.LogLevel.Info);
        NLogConfig.SaveXML(config.nLogConfig);
    }
    catch (Exception e) { logger.LogUsefulException(e); }
}

Practical Implementation Examples

Basic Load-Modify-Save Workflow

The standard pattern for manipulating settings follows a three-step sequence: load the existing state, apply the processing phase to ensure validation, modify properties, and persist changes.

// Load existing configuration or create defaults
var cfg = Configuration.Load();

// Apply validation, migration, and enrichment
Configuration.Process(ref cfg);

// Modify specific settings
cfg.useOnlinePac = true;
cfg.pacUrl = "https://example.com/proxy.pac";

// Persist to disk
Configuration.Save(cfg);

Adding Servers Programmatically

When integrating new server entries from UI dialogs or external sources, use the configuration's list management methods before saving.

Server newServer = new Server
{
    server = "192.168.1.100",
    server_port = 8388,
    password = "securePassword123",
    method = "aes-256-gcm",
    remarks = "Home Proxy"
};

Configuration.AddDefaultServerOrServer(cfg, newServer);
Configuration.Save(cfg);

Detecting Version Upgrades

The processing phase sets flags that downstream components can inspect to trigger version-specific onboarding or migration logic.

Configuration.Process(ref cfg);
if (cfg.firstRunOnNewVersion)
{
    MessageBox.Show($"Welcome to Shadowsocks {cfg.version}! Check the release notes for new features.");
}

Summary

  • The ** Configuration ** class in shadowsocks-csharp/Model/Configuration.cs provides the static trio Load(), Process(), and Save() to manage settings lifecycle.
  • Loading uses Newtonsoft.Json with ObjectCreationHandling.Replace and falls back to default constructors on any I/O or parsing error.
  • Processing validates geosite groups, corrects server indices, detects version upgrades, checks OS IPv6 support, validates proxy settings, and synchronizes NLog verbosity levels.
  • Saving reorders servers via SortByOnlineConfig(), writes indented JSON to gui-config.json, and persists NLog configuration changes atomically.
  • The system gracefully handles missing files, schema changes, and runtime environment variations without user intervention.

Frequently Asked Questions

Where does Shadowsocks Windows store its configuration file?

The application persists all user settings to ** gui-config.json ** in the executable directory. The Configuration class uses this filename (defined as CONFIG_FILE) for all read and write operations via the Load() and Save() methods.

What happens if the configuration file is corrupted or missing?

If File.Exists(CONFIG_FILE) returns false or if JsonConvert.DeserializeObject throws an exception during Load(), the system catches the error, logs it via LogUsefulException(), and returns a new Configuration() instance populated with hardcoded defaults from the constructor.

How does the system handle application version upgrades?

During Process(), the system compares UpdateChecker.Version against the stored config.version. If the application version is newer, it sets the boolean firstRunOnNewVersion to true, allowing UI components to display welcome dialogs or trigger migration scripts specific to the new release.

Why are servers reordered before saving?

The Save() method calls SortByOnlineConfig() to ensure that servers without an online configuration grouping appear first in the JSON array. This deterministic ordering prevents configuration drift when users mix manually configured servers with those fetched from remote subscription URLs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →