aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj3
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs2
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs9
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs12
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs20
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs31
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs72
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs20
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs42
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs102
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt11
-rw-r--r--tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template55
12 files changed, 334 insertions, 45 deletions
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj
index 0812e4c..1e011b1 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj
@@ -20,6 +20,9 @@
<None Update="hosts.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
+ <None Update="sing-config.json.template">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
</ItemGroup>
</Project>
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs
index 0b61cac..8f4c171 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs
@@ -16,6 +16,8 @@ public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSit
public record GeoSiteRuleEntry(HostMatchKind Kind, string Value, List<string> Attributes, string ContainingSite) : IGeoSiteEntry
{
public bool IsInclude => false;
+
+ public RoutingRuleMatcher GetRoutingRuleMatcher() => new(Kind, Value);
}
public record GeoSite(string Name, List<IGeoSiteEntry> Entries)
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs
index 5d2c504..858333d 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs
@@ -27,6 +27,10 @@ public static class HostMatchKindExtensions
public static List<HostMatchKind> DomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword, HostMatchKind.DomainRegex];
public static List<HostMatchKind> NonRegexDomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword];
+
+ public static List<HostMatchKind> SupportedInSingRouteMatchKinds { get; } = [..DomainMatchKinds, HostMatchKind.Ip];
+
+ public static bool IsSupportedInSingRoute(this HostMatchKind kind) => SupportedInSingRouteMatchKinds.Contains(kind);
}
public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List<string> Values);
@@ -117,8 +121,3 @@ public class HostMatchConfigFile
public string FileContent { get; }
public HostMatchConfig Config { get; }
}
-
-public class ProxyFile(string path) :
- HostMatchConfigFile(path, [.. Enum.GetValues<HostMatchKind>()], maxComponentCount: 0)
-{
-}
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs
index 9898af1..310143d 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs
@@ -74,16 +74,22 @@ public static class Program
GeoDataManager.Instance.Download(CrupestSecretToolDirectory, false);
return;
}
- else if (verb == "generate-surge-rule-set" || verb == "gs")
+ else if (verb == "generate-surge-rule-set" || verb == "gsr")
{
SurgeConfigGenerator.GenerateTo(
- Path.Join(CrupestSecretToolDirectory, "proxy.txt"),
+ CrupestSecretToolDirectory,
Path.Join(CrupestSecretToolDirectory, SurgeRuleSetChinaOutputFileName),
Path.Join(CrupestSecretToolDirectory, SurgeRuleSetGlobalOutputFileName),
- true, false
+ true, true
);
return;
}
+ else if (verb == "generate-sing-config" || verb == "gs")
+ {
+ var config = ToolConfig.FromDirectoryForSing(CrupestSecretToolDirectory, true, true);
+ Console.Out.WriteLine(config.ToSingConfigString());
+ return;
+ }
else if (verb == "generate" || verb == "g")
{
var config = ToolConfig.FromDirectory(CrupestSecretToolDirectory);
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs
index 638edb6..ddbbde8 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs
@@ -1,15 +1,21 @@
namespace Crupest.SecretTool;
-public abstract class Proxy(string tag) : IV4ConfigObject
+public abstract class Proxy(string tag) : IV4ConfigObject, ISingConfigObject
{
public string Tag { get; set; } = tag;
public abstract V4ConfigJsonObjects.Outbound ToJsonObjectV4();
+ public abstract SingConfigJsonObjects.OutboundBase ToJsonObjectSing();
object IV4ConfigObject.ToJsonObjectV4()
{
return ToJsonObjectV4();
}
+
+ object ISingConfigObject.ToJsonObjectSing()
+ {
+ return ToJsonObjectSing();
+ }
}
public class HttpProxy(string host, int port, string tag) : Proxy(tag)
@@ -17,6 +23,11 @@ public class HttpProxy(string host, int port, string tag) : Proxy(tag)
public string Host { get; set; } = host;
public int Port { get; set; } = port;
+ public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing()
+ {
+ throw new NotImplementedException("Http proxy is not supported in sing now.");
+ }
+
public override V4ConfigJsonObjects.Outbound ToJsonObjectV4()
{
return new V4ConfigJsonObjects.Outbound(Tag, "http",
@@ -34,6 +45,13 @@ public class VmessProxy(string host, int port, string userId, string path, strin
public string Path { get; set; } = path;
public string UserId { get; set; } = userId;
+ public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing()
+ {
+ return new SingConfigJsonObjects.VmessOutbound(Tag, Host, Port, UserId,
+ Transport: new SingConfigJsonObjects.V2rayWebsocketTransport(Path),
+ Tls: new SingConfigJsonObjects.OutboundTls(true));
+ }
+
public override V4ConfigJsonObjects.Outbound ToJsonObjectV4()
{
return new V4ConfigJsonObjects.Outbound(Tag, "vmess",
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs
new file mode 100644
index 0000000..81698a3
--- /dev/null
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs
@@ -0,0 +1,31 @@
+namespace Crupest.SecretTool;
+
+public class ProxyFile : HostMatchConfigFile
+{
+ public ProxyFile(string path) : base(path, [.. Enum.GetValues<HostMatchKind>()], maxComponentCount: 0)
+ {
+ RoutingRuleMatchers = Config.Items.Select(i => new RoutingRuleMatcher(i.Kind, i.MatchString)).ToList();
+ }
+
+ public List<RoutingRuleMatcher> RoutingRuleMatchers { get; }
+
+ public List<RoutingRuleMatcher> GetChinaRulesByGeoSite(GeoSiteData geoSiteData)
+ {
+ var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
+ return geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds, ["cn"]).Select(e => e.GetRoutingRuleMatcher()).ToList();
+ }
+
+ public List<RoutingRuleMatcher> GetRulesFlattenGeoSite(GeoSiteData geoSiteData, bool noCn = false)
+ {
+ var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
+ var flattenGeoSiteRules = geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds)
+ .Where(e => !noCn || !e.Attributes.Contains("cn"))
+ .Select(e => e.GetRoutingRuleMatcher())
+ .ToList();
+ var otherRules = RoutingRuleMatchers.Where(m => m.MatchKind != HostMatchKind.GeoSite).ToList();
+ return [
+ ..flattenGeoSiteRules,
+ ..otherRules
+ ];
+ }
+}
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs
index dbced0e..9c247a2 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs
@@ -1,5 +1,10 @@
namespace Crupest.SecretTool;
+public record RoutingRuleMatcher(HostMatchKind MatchKind, string MatchString)
+{
+ public RoutingRule ToRoutingRule(string OutboundTag) => new(MatchKind, MatchString, OutboundTag);
+}
+
public record RoutingRule(HostMatchKind MatchKind, string MatchString, string OutboundTag) : IV4ConfigObject
{
public string ToolConfigString => MatchKind switch
@@ -14,6 +19,8 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou
_ => throw new ArgumentException("Invalid matcher kind.")
};
+ public string ToolConfigStringSing => MatchKind.IsSupportedInSingRoute() ? MatchString : throw new ArgumentException("Unsupported matcher kind for sing.");
+
public static Dictionary<string, List<RoutingRule>> GroupByOutboundTag(List<RoutingRule> rules)
=> rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary();
@@ -23,6 +30,34 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou
public static List<List<RoutingRule>> GroupByOutboundTagAndMatcherKind(List<RoutingRule> rules)
=> GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList();
+ public static SingConfigJsonObjects.RouteRule ListToJsonObjectSing(List<RoutingRule> rules)
+ {
+ if (rules.Count == 0)
+ {
+ throw new ArgumentException("Rule list is empty.");
+ }
+
+ var outboundTag = rules[0].OutboundTag;
+
+ if (rules.Any(r => !r.MatchKind.IsSupportedInSingRoute()))
+ {
+ throw new ArgumentException("Rules must have matcher kinds supported in sing.");
+ }
+
+ if (rules.Any(r => r.OutboundTag != outboundTag))
+ {
+ throw new ArgumentException("Rules must have the same outbound tag.");
+ }
+
+ return new SingConfigJsonObjects.RouteRule(Outbound: outboundTag,
+ Domain: rules.Where(r => r.MatchKind == HostMatchKind.DomainFull).Select(r => r.ToolConfigStringSing).ToList(),
+ DomainSuffix: rules.Where(r => r.MatchKind == HostMatchKind.DomainSuffix).Select(r => r.ToolConfigStringSing).ToList(),
+ DomainKeyword: rules.Where(r => r.MatchKind == HostMatchKind.DomainKeyword).Select(r => r.ToolConfigStringSing).ToList(),
+ DomainRegex: rules.Where(r => r.MatchKind == HostMatchKind.DomainRegex).Select(r => r.ToolConfigStringSing).ToList(),
+ IpCidr: rules.Where(r => r.MatchKind == HostMatchKind.Ip).Select(r => r.ToolConfigStringSing).ToList()
+ );
+ }
+
public static V4ConfigJsonObjects.RoutingRule ListToJsonObject(List<RoutingRule> rules)
{
if (rules.Count == 0)
@@ -56,12 +91,14 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou
return new RoutingRule(HostMatchKind.GeoSite, $"{MatchString}@cn", outboundTag);
}
+ public RoutingRuleMatcher GetMatcher() => new(MatchKind, MatchString);
+
public V4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]);
object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
}
-public record Routing(List<RoutingRule> Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV4ConfigObject
+public record Routing(List<RoutingRule> Rules) : IV4ConfigObject, ISingConfigObject
{
public List<RoutingRule> CreateGeositeCnDirectRules()
{
@@ -69,7 +106,14 @@ public record Routing(List<RoutingRule> Rules, bool DirectGeositeCn = true, stri
.Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList();
}
- public V4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true)
+ public SingConfigJsonObjects.Route ToJsonObjectSing()
+ {
+ List<SingConfigJsonObjects.RouteRule> ruleJsonObjects = [];
+ ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTag(Rules).Values.Select(RoutingRule.ListToJsonObjectSing));
+ return new SingConfigJsonObjects.Route(ruleJsonObjects);
+ }
+
+ public V4ConfigJsonObjects.Routing ToJsonObjectV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true)
{
List<V4ConfigJsonObjects.RoutingRule> ruleJsonObjects = [];
@@ -80,18 +124,32 @@ public record Routing(List<RoutingRule> Rules, bool DirectGeositeCn = true, stri
ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(RoutingRule.ListToJsonObject));
- return new V4ConfigJsonObjects.Routing(ruleJsonObjects);
+ return new V4ConfigJsonObjects.Routing(ruleJsonObjects, domainStrategy);
}
object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
- public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag, bool directGeositeCn)
+ object ISingConfigObject.ToJsonObjectSing() => ToJsonObjectSing();
+
+ public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag)
{
+ return new Routing(
+ proxyFile.RoutingRuleMatchers.Select(m => m.ToRoutingRule(outboundTag)).ToList());
+ }
+
+ public static Routing FromProxyFileForSing(ProxyFile proxyFile, GeoSiteData geoSiteData, string outboundTag, string? directCnOutboundTag = null)
+ {
+ List<RoutingRule> rules = [];
+
+ if (directCnOutboundTag is not null)
+ {
+ rules.AddRange(proxyFile.GetChinaRulesByGeoSite(geoSiteData).Select(m => m.ToRoutingRule(directCnOutboundTag)).ToList());
+ }
+
+ rules.AddRange(proxyFile.GetRulesFlattenGeoSite(geoSiteData).Where(m => m.MatchKind.IsSupportedInSingRoute()).Select(m => m.ToRoutingRule(outboundTag)).ToList());
return new Routing(
- proxyFile.Config.Items.Select(
- i => new RoutingRule(i.Kind, i.MatchString, outboundTag)).ToList(),
- directGeositeCn
+ rules
);
}
}
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs
new file mode 100644
index 0000000..6af0cd1
--- /dev/null
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs
@@ -0,0 +1,20 @@
+namespace Crupest.SecretTool;
+
+public static class SingConfigJsonObjects
+{
+ public interface IObject;
+
+ public record OutboundTls(bool Enabled);
+ public record V2rayTransportBase(string Type);
+ public record V2rayWebsocketTransport(string Path, Dictionary<string, string>? Headers = null) : V2rayTransportBase("ws");
+ public record OutboundBase(string Tag, string Type) : IObject;
+ public record VmessOutbound(string Tag, string Server, int ServerPort, string Uuid, string Security = "auto",
+ V2rayTransportBase? Transport = null, OutboundTls? Tls = null): OutboundBase(Tag, "vmess");
+
+ public record RouteRule(List<string>? Domain = null, List<string>? DomainSuffix = null, List<string>? DomainKeyword = null,
+ List<string>? DomainRegex = null, List<string>? IpCidr = null, List<string>? SourceIpCidr = null,
+ List<int>? Port = null, List<int>? SourcePort = null, List<string>? PortRange = null, List<string>? SourcePortRange = null,
+ string? Network = null, List<string>? Inbound = null, string? Outbound = null) : IObject;
+
+ public record Route(List<RouteRule> Rules) : IObject;
+}
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs
index 451db3e..8a57c9f 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs
@@ -1,9 +1,9 @@
namespace Crupest.SecretTool;
-public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData)
+public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoData)
{
- public ProxyFile ProxyFile { get; } = proxyFile;
- public GeoSiteData GeoSiteData { get; } = geoSiteData;
+ public ProxyFile ProxyFile => proxyFile;
+ public GeoSiteData GeoData => geoData;
private static string ToSurgeRuleString(HostMatchKind kind, string value)
{
@@ -19,39 +19,35 @@ public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData)
return $"{ruleType},{value}";
}
- private static List<HostMatchKind> DomainMatcherKinds { get; } = [
- HostMatchKind.DomainFull, HostMatchKind.DomainKeyword,
- HostMatchKind.DomainRegex, HostMatchKind.DomainSuffix,
- ];
+ public static string GenerateSurgeRuleSetString(List<RoutingRuleMatcher> rules)
+ {
+ return string.Join('\n', rules.Select(r => ToSurgeRuleString(r.MatchKind, r.MatchString)));
+ }
public string GenerateChinaRuleSet()
{
- var geoSites = ProxyFile.Config.Items.Where(i => i.Kind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
- var cnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds, ["cn"]).ToList();
- return string.Join('\n', cnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value)));
+ return GenerateSurgeRuleSetString(proxyFile.GetChinaRulesByGeoSite(GeoData));
}
public string GenerateGlobalRuleSet()
{
- var geoSites = ProxyFile.Config.Items.Where(i => i.Kind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList();
- var nonCnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds).Where(e => !e.Attributes.Contains("cn")).ToList();
- var domainRules = ProxyFile.Config.Items.Where(i => DomainMatcherKinds.Contains(i.Kind)).ToList();
- return string.Join('\n', [
- ..nonCnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value)),
- ..domainRules.Select(r => ToSurgeRuleString(r.Kind, r.MatchString))
- ]);
+ return GenerateSurgeRuleSetString(proxyFile.GetRulesFlattenGeoSite(geoData, true));
}
- public static SurgeConfigGenerator Create(string proxyFilePath, bool clean, bool silent)
+ public static void GenerateTo(ProxyFile proxyFile, GeoSiteData geoSiteData, string cnPath, string globalPath, bool silent)
{
- var proxyFile = new ProxyFile(proxyFilePath);
- var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent);
- return new SurgeConfigGenerator(proxyFile, geoSiteData);
+ var generator = new SurgeConfigGenerator(proxyFile, geoSiteData);
+ File.WriteAllText(cnPath, generator.GenerateChinaRuleSet());
+ if (!silent) Console.WriteLine($"China rule set written to {cnPath}.");
+ File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet());
+ if (!silent) Console.WriteLine($"Global rule set written to {globalPath}.");
}
- public static void GenerateTo(string proxyFilePath, string cnPath, string globalPath, bool clean, bool silent)
+ public static void GenerateTo(string directory, string cnPath, string globalPath, bool clean, bool silent)
{
- var generator = Create(proxyFilePath, clean, silent);
+ var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent);
+ var proxyFile = new ProxyFile(Path.Combine(directory, ToolConfig.ProxyConfigFileName));
+ var generator = new SurgeConfigGenerator(proxyFile, geoSiteData);
File.WriteAllText(cnPath, generator.GenerateChinaRuleSet());
if (!silent) Console.WriteLine($"China rule set written to {cnPath}.");
File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet());
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs
index 4fe9a40..534bd65 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs
@@ -8,6 +8,11 @@ public interface IV4ConfigObject
object ToJsonObjectV4();
}
+public interface ISingConfigObject
+{
+ object ToJsonObjectSing();
+}
+
public class ToolConfig(Template template, List<Proxy> proxies, Routing router, StaticHosts? hosts)
{
private class JsonInterfaceConverter<Interface> : JsonConverter<Interface>
@@ -35,11 +40,14 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
public const string ProxyConfigFileName = "proxy.txt";
public const string HostsConfigFileName = "hosts.txt";
+ public const string SingConfigTemplateFileName = "sing-config.json.template";
+
public static List<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName];
public static List<string> ConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, HostsConfigFileName];
private const string ProxyAnchor = "PROXY_ANCHOR";
private const string RoutingAnchor = "ROUTING_ANCHOR";
+ private const string SingRouteAnchor = "ROUTE_ANCHOR";
private const string HostsAnchor = "HOSTS_ANCHOR";
public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V2RAY_GEOSITE_USE_CN";
@@ -55,7 +63,41 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
public Routing Routing { get; set; } = router;
public StaticHosts Hosts { get; set; } = hosts is null ? new StaticHosts([]) : hosts;
- public string ToJsonStringV4(bool pretty = true)
+ public string ToSingConfigString(bool pretty = true)
+ {
+ var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ });
+ // TODO: Make interface converter generic.
+ jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.OutboundBase>());
+ jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.V2rayTransportBase>());
+
+ var templateValues = new Dictionary<string, string>
+ {
+ [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectSing(), jsonOptions))),
+ [SingRouteAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectSing(), jsonOptions),
+ };
+
+ var configString = Template.Generate(templateValues);
+
+ if (pretty)
+ {
+ var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions)
+ {
+ WriteIndented = true,
+ };
+ return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(configString, jsonOptionsPretty), jsonOptionsPretty);
+ }
+ else
+ {
+ return configString;
+ }
+ }
+
+ public string ToJsonStringV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true, bool pretty = true)
{
var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
{
@@ -70,7 +112,7 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
var templateValues = new Dictionary<string, string>
{
[ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))),
- [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions),
+ [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(domainStrategy, directGeositeCn), jsonOptions),
[HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions),
};
@@ -125,7 +167,7 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
file = vmessPath;
var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy");
file = proxyPath;
- var routing = Routing.FromProxyFile(proxyFile, "proxy", UseCnGeoSite);
+ var routing = Routing.FromProxyFile(proxyFile, "proxy");
file = hostsPath ?? "";
var hosts = hostsString is not null ? StaticHosts.CreateFromHostMatchConfigString(hostsString) : null;
return new ToolConfig(template, [vmess], routing, hosts);
@@ -136,6 +178,50 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
}
}
+ public static ToolConfig FromFilesForSing(string templatePath, string vmessPath, string proxyPath, bool clean, bool silent)
+ {
+ foreach (var path in new List<string>([templatePath, vmessPath, proxyPath]))
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Required config file not found: {path}.");
+ }
+ }
+
+ var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent);
+
+ ProxyFile proxyFile = new(proxyPath);
+ string templateString, vmessString;
+
+ string file = "";
+ try
+ {
+ file = templatePath;
+ templateString = File.ReadAllText(templatePath);
+ file = vmessPath;
+ vmessString = File.ReadAllText(vmessPath);
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"Error reading config file {file}.", e);
+ }
+
+ try
+ {
+ file = templatePath;
+ var template = new Template(templateString);
+ file = vmessPath;
+ var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy");
+ file = proxyPath;
+ var routing = Routing.FromProxyFileForSing(proxyFile, geoSiteData, "proxy", "direct");
+ return new ToolConfig(template, [vmess], routing, null);
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"Error parsing config file {file}.", e);
+ }
+ }
+
public static ToolConfig FromDirectory(string directory)
{
return FromFiles(
@@ -146,6 +232,16 @@ public class ToolConfig(Template template, List<Proxy> proxies, Routing router,
);
}
+ public static ToolConfig FromDirectoryForSing(string directory, bool clean, bool silent)
+ {
+ return FromFilesForSing(
+ Path.Join(directory, SingConfigTemplateFileName),
+ Path.Join(directory, VmessConfigFileName),
+ Path.Join(directory, ProxyConfigFileName),
+ clean, silent
+ );
+ }
+
public static void FromDirectoryAndWriteToFile(string directory, string outputPath)
{
var config = FromDirectory(directory);
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt
index 6273e35..39800f9 100644
--- a/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt
@@ -1,20 +1,21 @@
-GeoSite github
+GeoSite microsoft
GeoSite google
GeoSite youtube
-GeoSite twitter
+GeoSite x
GeoSite facebook
GeoSite discord
GeoSite reddit
GeoSite twitch
-GeoSite onedrive
GeoSite quora
GeoSite telegram
GeoSite imgur
GeoSite stackexchange
+GeoSite medium
GeoSite duckduckgo
GeoSite wikimedia
GeoSite gitbook
+GeoSite github
GeoSite gitlab
GeoSite sourceforge
GeoSite creativecommons
@@ -33,6 +34,8 @@ GeoSite v2ray
GeoSite homebrew
GeoSite azure
+GeoSite akamai
+GeoSite aws
GeoSite jsdelivr
GeoSite fastly
GeoSite heroku
@@ -43,3 +46,5 @@ GeoSite ieee
GeoSite sci-hub
GeoSite libgen
GeoSite z-library
+
+sagernet.org
diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template b/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template
new file mode 100644
index 0000000..429bd1d
--- /dev/null
+++ b/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template
@@ -0,0 +1,55 @@
+{
+ "log": {
+ "disabled": false,
+ "level": "info",
+ "timestamp": true
+ },
+ "dns": {
+ "servers": [
+ {
+ "tag": "ali-doh",
+ "address": "https://dns.alidns.com/dns-query",
+ "address_resolver": "ali"
+ },
+ {
+ "tag": "ali",
+ "address": "223.5.5.5"
+ },
+ {
+ "tag": "cloudflare",
+ "address": "1.1.1.1"
+ },
+ {
+ "tag": "google",
+ "address": "8.8.8.8"
+ }
+
+ ]
+ },
+ "inbounds": [
+ {
+ "tag": "http-in",
+ "type": "http",
+ "listen": "127.0.0.1",
+ "listen_port": 3080
+ },
+ {
+ "tag": "socks-in",
+ "type": "socks",
+ "listen": "127.0.0.1",
+ "listen_port": 3081
+ }
+ ],
+ "outbounds": [
+ {
+ "type": "direct",
+ "tag": "direct"
+ },
+ {
+ "type": "block",
+ "tag": "block"
+ },
+ ${PROXY_ANCHOR}
+ ],
+ "route": ${ROUTE_ANCHOR}
+}