diff options
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}
+}
|