Browse Source

📐 PAC: add options for direct and proxied groups

- Configuration: geositeDirectGroups + geositeProxiedGroups + geositePreferDirect
- PAC: rule generation using these new groups + cleanup
tags/4.3.0.0
database64128 4 years ago
parent
commit
69b3012508
No known key found for this signature in database GPG Key ID: 1CA27546BEDB8B01
4 changed files with 201 additions and 70 deletions
  1. +137
    -49
      shadowsocks-csharp/Controller/Service/GeositeUpdater.cs
  2. +2
    -2
      shadowsocks-csharp/Controller/Service/PACDaemon.cs
  3. +1
    -1
      shadowsocks-csharp/Controller/ShadowsocksController.cs
  4. +61
    -18
      shadowsocks-csharp/Model/Configuration.cs

+ 137
- 49
shadowsocks-csharp/Controller/Service/GeositeUpdater.cs View File

@@ -39,18 +39,12 @@ namespace Shadowsocks.Controller
private static HttpClient httpClient;
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 readonly DomainObject.Types.Attribute geositeExcludeAttribute;
private static byte[] geositeDB;

public static readonly Dictionary<string, IList<DomainObject>> Geosites = new Dictionary<string, IList<DomainObject>>();

static GeositeUpdater()
{
geositeExcludeAttribute = new DomainObject.Types.Attribute
{
Key = "cn",
BoolValue = true
};
if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0)
{
geositeDB = File.ReadAllBytes(DATABASE_PATH);
@@ -87,8 +81,7 @@ namespace Shadowsocks.Controller
string geositeSha256sumUrl = GEOSITE_SHA256SUM_URL;
SHA256 mySHA256 = SHA256.Create();
var config = Program.MainController.GetCurrentConfiguration();
string group = config.geositeGroup;
bool blacklist = config.geositeBlacklistMode;
bool blacklist = config.geositePreferDirect;
if (!string.IsNullOrWhiteSpace(config.geositeUrl))
{
@@ -154,7 +147,7 @@ namespace Shadowsocks.Controller
// update stuff
geositeDB = downloadedBytes;
LoadGeositeList();
bool pacFileChanged = MergeAndWritePACFile(group, blacklist);
bool pacFileChanged = MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, blacklist);
UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged));
}
catch (Exception ex)
@@ -176,10 +169,17 @@ namespace Shadowsocks.Controller
}
}

public static bool MergeAndWritePACFile(string group, bool blacklist)
/// <summary>
/// Merge and write pac.txt from geosite.
/// Used at multiple places.
/// </summary>
/// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
/// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
/// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
/// <returns></returns>
public static bool MergeAndWritePACFile(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
{
IList<DomainObject> domains = Geosites[group];
string abpContent = MergePACFile(domains, blacklist);
string abpContent = MergePACFile(directGroups, proxiedGroups, blacklist);
if (File.Exists(PACDaemon.PAC_FILE))
{
string original = FileManager.NonExclusiveReadAllText(PACDaemon.PAC_FILE, Encoding.UTF8);
@@ -197,9 +197,39 @@ namespace Shadowsocks.Controller
/// </summary>
/// <param name="group">The group name to check for.</param>
/// <returns>True if the group exists. False if the group doesn't exist.</returns>
public static bool CheckGeositeGroup(string group) => Geosites.ContainsKey(group);
public static bool CheckGeositeGroup(string group) => SeparateAttributeFromGroupName(group, out string groupName, out _) && Geosites.ContainsKey(groupName);

/// <summary>
/// Separates the attribute (e.g. @cn) from a group name.
/// No checks are performed.
/// </summary>
/// <param name="group">A group name potentially with a trailing attribute.</param>
/// <param name="groupName">The group name with the attribute stripped.</param>
/// <param name="attribute">The attribute.</param>
/// <returns>True for success. False for more than one '@'.</returns>
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(IList<DomainObject> domains, bool blacklist)
private static string MergePACFile(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
{
string abpContent;
if (File.Exists(PACDaemon.USER_ABP_FILE))
@@ -215,18 +245,18 @@ namespace Shadowsocks.Controller
if (File.Exists(PACDaemon.USER_RULE_FILE))
{
string userrulesString = FileManager.NonExclusiveReadAllText(PACDaemon.USER_RULE_FILE, Encoding.UTF8);
userruleLines = PreProcessGFWList(userrulesString);
userruleLines = ProcessUserRules(userrulesString);
}

List<string> gfwLines = GeositeToGFWList(domains, blacklist);
List<string> ruleLines = GenerateRules(directGroups, proxiedGroups, blacklist);
abpContent =
$@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(ruleLines, Formatting.Indented)};
{abpContent}";
return abpContent;
}

private static List<string> PreProcessGFWList(string content)
private static List<string> ProcessUserRules(string content)
{
List<string> valid_lines = new List<string>();
using (var stringReader = new StringReader(content))
@@ -241,47 +271,105 @@ var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)};
return valid_lines;
}

private static List<string> GeositeToGFWList(IList<DomainObject> domains, bool blacklist)
/// <summary>
/// Generates rule lines based on user preference.
/// </summary>
/// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
/// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
/// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateRules(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
{
return blacklist ? GeositeToGFWListBlack(domains) : GeositeToGFWListWhite(domains);
List<string> ruleLines;
if (blacklist) // blocking + exception rules
{
ruleLines = GenerateBlockingRules(proxiedGroups);
ruleLines.AddRange(GenerateExceptionRules(directGroups));
}
else // proxy all + exception rules
{
ruleLines = new List<string>()
{
"/.*/" // block/proxy all unmatched domains
};
ruleLines.AddRange(GenerateExceptionRules(directGroups));
}
return ruleLines;
}

private static List<string> GeositeToGFWListBlack(IList<DomainObject> domains)
/// <summary>
/// Generates rules that match domains that should be proxied.
/// </summary>
/// <param name="groups">A list of source groups.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateBlockingRules(List<string> groups)
{
List<string> ret = new List<string>(domains.Count + 100);// 100 overhead
foreach (var d in domains)
List<string> ruleLines = new List<string>();
foreach (var group in groups)
{
if (d.Attribute.Contains(geositeExcludeAttribute))
continue;

string domain = d.Value;

switch (d.Type)
// separate group name and attribute
SeparateAttributeFromGroupName(group, out string groupName, out string attribute);
var domainObjects = Geosites[groupName];
if (!string.IsNullOrEmpty(attribute)) // has attribute
{
case DomainObject.Types.Type.Plain:
ret.Add(domain);
break;
case DomainObject.Types.Type.Regex:
ret.Add($"/{domain}/");
break;
case DomainObject.Types.Type.Domain:
ret.Add($"||{domain}");
break;
case DomainObject.Types.Type.Full:
ret.Add($"|http://{domain}");
ret.Add($"|https://{domain}");
break;
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 ret;
return ruleLines;
}

private static List<string> GeositeToGFWListWhite(IList<DomainObject> domains)
{
return GeositeToGFWListBlack(domains)
.Select(r => $"@@{r}") // convert to whitelist
.Prepend("/.*/") // blacklist all other site
/// <summary>
/// Generates rules that match domains that should be connected directly without a proxy.
/// </summary>
/// <param name="groups">A list of source groups.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateExceptionRules(List<string> groups)
=> GenerateBlockingRules(groups)
.Select(r => $"@@{r}") // convert blocking rules to exception rules
.ToList();
}
}
}

+ 2
- 2
shadowsocks-csharp/Controller/Service/PACDaemon.cs View File

@@ -45,7 +45,7 @@ namespace Shadowsocks.Controller
{
if (!File.Exists(PAC_FILE))
{
GeositeUpdater.MergeAndWritePACFile(config.geositeGroup, config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect);
}
return PAC_FILE;
}
@@ -63,7 +63,7 @@ namespace Shadowsocks.Controller
{
if (!File.Exists(PAC_FILE))
{
GeositeUpdater.MergeAndWritePACFile(config.geositeGroup, config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect);
}
return File.ReadAllText(PAC_FILE, Encoding.UTF8);
}


+ 1
- 1
shadowsocks-csharp/Controller/ShadowsocksController.cs View File

@@ -357,7 +357,7 @@ namespace Shadowsocks.Controller
private static readonly IEnumerable<char> IgnoredLineBegins = new[] { '!', '[' };
private void PacDaemon_UserRuleFileChanged(object sender, EventArgs e)
{
GeositeUpdater.MergeAndWritePACFile(_config.geositeGroup, _config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(_config.geositeDirectGroups, _config.geositeProxiedGroups, _config.geositePreferDirect);
UpdateSystemProxy();
}


+ 61
- 18
shadowsocks-csharp/Model/Configuration.cs View File

@@ -44,10 +44,11 @@ namespace Shadowsocks.Model
// hidden options
public bool isIPv6Enabled; // for experimental ipv6 support
public bool generateLegacyUrl = false; // for pre-sip002 url compatibility
public bool generateLegacyUrl; // for pre-sip002 url compatibility
public string geositeUrl; // for custom geosite source (and rule group)
public string geositeGroup;
public bool geositeBlacklistMode;
public List<string> geositeDirectGroups; // groups of domains that we connect without the proxy
public List<string> 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;
@@ -83,8 +84,16 @@ namespace Shadowsocks.Model
isIPv6Enabled = false;
generateLegacyUrl = false;
geositeUrl = "";
geositeGroup = "geolocation-!cn";
geositeBlacklistMode = true;
geositeDirectGroups = new List<string>()
{
"cn",
"geolocation-!cn@cn"
};
geositeProxiedGroups = new List<string>()
{
"geolocation-!cn"
};
geositePreferDirect = false;
userAgent = "ShadowsocksWindows/$version";
logViewer = new LogViewerConfig();
@@ -154,7 +163,10 @@ namespace Shadowsocks.Model
try
{
string configContent = File.ReadAllText(CONFIG_FILE);
config = JsonConvert.DeserializeObject<Configuration>(configContent);
config = JsonConvert.DeserializeObject<Configuration>(configContent, new JsonSerializerSettings()
{
ObjectCreationHandling = ObjectCreationHandling.Replace
});
return config;
}
catch (Exception e)
@@ -173,18 +185,12 @@ namespace Shadowsocks.Model
/// <param name="config">A reference of Configuration object.</param>
public static void Process(ref Configuration config)
{
// Verify if the configured geosite group exists.
// Reset to default if no such group.
if (!GeositeUpdater.CheckGeositeGroup(config.geositeGroup))
{
#if DEBUG
logger.Debug($"Current group: {config.geositeGroup}");
foreach (var group in GeositeUpdater.Geosites.Keys)
logger.Debug($"{group}");
#endif
config.geositeGroup = "geolocation-!cn";
logger.Warn("The specified Geosite group doesn't exist. Using default group.");
}
// 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.
if (UpdateChecker.Asset.CompareVersion(UpdateChecker.Version, config.version ?? "0") > 0)
@@ -273,6 +279,43 @@ namespace Shadowsocks.Model
return ret;
}
/// <summary>
/// Validates if the groups in the list are all valid.
/// </summary>
/// <param name="groups">The list of groups to validate.</param>
/// <returns>
/// True if all groups are valid.
/// False if any one of them is invalid.
/// </returns>
public static bool ValidateGeositeGroupList(List<string> 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<string> geositeDirectGroups)
{
geositeDirectGroups.Clear();
geositeDirectGroups.Add("cn");
geositeDirectGroups.Add("geolocation-!cn@cn");
}
public static void ResetGeositeProxiedGroup(ref List<string> geositeProxiedGroups)
{
geositeProxiedGroups.Clear();
geositeProxiedGroups.Add("geolocation-!cn");
}
public static Server AddDefaultServerOrServer(Configuration config, Server server = null, int? index = null)
{
if (config?.configs != null)


Loading…
Cancel
Save