diff options
| author | crupest <crupest@outlook.com> | 2024-09-17 23:59:44 +0800 | 
|---|---|---|
| committer | crupest <crupest@outlook.com> | 2024-09-25 23:01:52 +0800 | 
| commit | a10b6b1ab484c4d541e027c070793bcf374f2ec9 (patch) | |
| tree | a6a4c426983929ef8824c378aa283794737bfa8d | |
| parent | 8b4d2d23729897ffe33f5177717f1c0e502eb821 (diff) | |
| download | crupest-a10b6b1ab484c4d541e027c070793bcf374f2ec9.tar.gz crupest-a10b6b1ab484c4d541e027c070793bcf374f2ec9.tar.bz2 crupest-a10b6b1ab484c4d541e027c070793bcf374f2ec9.zip | |
refactor(secret): add cn geosite direct. refactor geodata manager.
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs | 42 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs | 209 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/Program.cs | 30 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs | 10 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs | 13 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs | 43 | ||||
| -rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/config.json.template | 4 | 
7 files changed, 287 insertions, 64 deletions
| diff --git a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs deleted file mode 100644 index d3efc1f..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace Crupest.V2ray; - -public class GeoDataDownloader -{ -    public record GithubReleaseAsset(string ResourceName, string User, string Repo, string AssetName, string Output); - -    public GeoDataDownloader() -    { -        Resources = new() -        { -            new("geosite", "v2fly", "domain-list-community", "dlc.dat", "geosite.dat"), -            new("geoip", "v2fly", "geoip", "geoip.dat", "geoip.dat"), -            new("geosite", "v2fly", "geoip", "geoip-only-cn-private.dat", "geoip-only-cn-private.dat") -        }; -    } - -    public List<GithubReleaseAsset> Resources { get; set; } - -    public static string GetReleaseFileUrl(string user, string repo, string assetName) -    { -        return $"https://github.com/{user}/{repo}/releases/latest/download/{assetName}"; -    } - -    public static void GithubDownload(HttpClient httpClient, string user, string repo, string assetName, string outputPath) -    { -        using var responseStream = httpClient.GetStreamAsync(GetReleaseFileUrl(user, repo, assetName)).Result; -        using var outputFileStream = File.OpenWrite(outputPath); -        responseStream.CopyTo(outputFileStream); -    } - -    public void Download(string outputDir) -    { -        using var httpClient = new HttpClient(); - -        foreach (var resource in Resources) -        { -            Console.WriteLine($"Downloading {resource.ResourceName}..."); -            GithubDownload(httpClient, resource.User, resource.Repo, resource.AssetName, Path.Combine(outputDir, resource.Output)); -            Console.WriteLine($"Downloaded {resource.ResourceName}!"); -        } -    } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs new file mode 100644 index 0000000..75f071b --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs @@ -0,0 +1,209 @@ +using System.IO.Compression; + +namespace Crupest.V2ray; + +public interface IGeoSiteEntry +{ +    bool IsInclude { get; } +    string Value { get; } +} + +public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSiteEntry +{ +    public bool IsInclude => true; +} + +public record GeoSiteRuleEntry(V2rayHostMatcherKind Kind, string Value, List<string> Attributes, string ContainingSite) : IGeoSiteEntry +{ +    public bool IsInclude => false; +} + +public record GeoSite(string Name, List<IGeoSiteEntry> Entries) +{ +    public static GeoSite Parse(string name, string str) +    { +        List<IGeoSiteEntry> entries = []; +        var listConfig = new ListConfig(str); +        foreach (var item in listConfig.Config) +        { +            var (value, line) = item; + +            if (value.StartsWith("include:")) +            { +                var include = value["include:".Length..].Trim(); +                if (include.Length == 0 || include.Contains(' ')) +                { +                    throw new FormatException($"Invalid geo site rule in line {line}. Invalid include value."); +                } +                entries.Add(new GeoSiteIncludeEntry(include, name)); +                continue; +            } + +            var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); +            if (segments.Length > 2) +            { +                throw new FormatException($"Invalid geo site rule in line {line}. More than one ':'."); +            } + +            V2rayHostMatcherKind kind; +            if (segments.Length == 2) +            { +                kind = segments[0] switch +                { +                    "domain" => kind = V2rayHostMatcherKind.DomainSuffix, +                    "full" => kind = V2rayHostMatcherKind.DomainFull, +                    "keyword" => kind = V2rayHostMatcherKind.DomainKeyword, +                    "regexp" => kind = V2rayHostMatcherKind.DomainRegex, +                    _ => throw new FormatException($"Invalid geo site rule in line {line}. Unknown matcher.") +                }; +            } +            else +            { +                kind = V2rayHostMatcherKind.DomainSuffix; +            } + +            var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries); +            var domain = domainSegments[0]; +            if (Uri.CheckHostName(domain) != UriHostNameType.Dns) +            { +                throw new FormatException($"Invalid geo site rule in line {line}. Invalid domain."); +            } + +            List<string> attributes = []; +            foreach (var s in domainSegments) +            { +                if (s.Length == 0) +                { +                    throw new FormatException($"Invalid geo site rule in line {line}. Empty attribute value."); +                } +                attributes.Add(s); +            } + +            entries.Add(new GeoSiteRuleEntry(kind, domain, attributes, name)); +        } +        return new GeoSite(name, entries); +    } +} + +public class GeoSiteDataParser(string directory) +{ +    private static List<GeoSite> Parse(string directory) +    { +        var sites = new List<GeoSite>(); +        foreach (var file in Directory.GetFileSystemEntries(directory)) +        { +            var path = Path.Combine(directory, file); +            var content = File.ReadAllText(path); +            sites.Add(GeoSite.Parse(file, content)); +        } +        return sites; +    } + +    public string DataDirectory { get; } = directory; + +    public List<GeoSite> Sites { get; } = Parse(directory); +} + +public class GeoDataManager +{ +    public const string GeoSiteFileName = "geosite.dat"; +    public const string GeoIpFileName = "geoip.dat"; +    public const string GeoIpCnFileName = "geoip-only-cn-private.dat"; +    public const string V2rayGithubOrganization = "v2fly"; +    public const string V2rayGeoSiteGithubRepository = "domain-list-community"; +    public const string V2rayGeoIpGithubRepository = "geoip"; +    public const string V2rayGeoSiteCnGithubReleaseFilename = "dlc.dat"; +    public const string V2rayGeoIpGithubReleaseFilename = "geoip.dat"; +    public const string V2rayGeoIpCnGithubReleaseFilename = "geoip-only-cn-private.dat"; + +    public static GeoDataManager Instance { get; } = new GeoDataManager(); + +    public record GeoDataAsset(string Name, string FileName, string GithubUser, string GithubRepo, string GithubReleaseFileName); + +    public GeoDataManager() +    { +        Assets = +        [ +            new("geosite", GeoSiteFileName, V2rayGithubOrganization, V2rayGeoSiteGithubRepository, V2rayGeoSiteGithubRepository), +            new("geoip", GeoIpFileName, V2rayGithubOrganization, V2rayGeoIpGithubRepository, V2rayGeoIpGithubReleaseFilename), +            new("geoip-cn", GeoIpCnFileName, V2rayGithubOrganization, V2rayGeoIpGithubRepository, V2rayGeoIpCnGithubReleaseFilename), +        ]; +    } + +    public List<GeoDataAsset> Assets { get; set; } + +    private static string GetReleaseFileUrl(string user, string repo, string fileName) +    { +        return $"https://github.com/{user}/{repo}/releases/latest/download/{fileName}"; +    } + +    private static void GithubDownloadRelease(HttpClient httpClient, string user, string repo, string fileName, string outputPath) +    { +        using var responseStream = httpClient.GetStreamAsync(GetReleaseFileUrl(user, repo, fileName)).Result; +        using var outputFileStream = File.OpenWrite(outputPath); +        responseStream.CopyTo(outputFileStream); +    } + +    public bool HasAllAssets(string directory, out List<string> missing) +    { +        missing = []; +        foreach (var asset in Assets) +        { +            var assetPath = Path.Combine(directory, asset.FileName); +            if (!File.Exists(assetPath)) +            { +                missing.Add(asset.Name); +            } +        } +        return missing.Count == 0; +    } + +    public void Download(string outputDir, bool silent) +    { +        using var httpClient = new HttpClient(); + +        foreach (var asset in Assets) +        { +            if (!silent) +            { +                Console.WriteLine($"Downloading {asset.Name}..."); +            } +            GithubDownloadRelease(httpClient, asset.GithubUser, asset.GithubRepo, asset.GithubReleaseFileName, Path.Combine(outputDir, asset.FileName)); +            if (!silent) +            { +                Console.WriteLine($"Downloaded {asset.Name}!"); +            } +        } +    } + +    private static string GetGithubRepositoryArchiveUrl(string user, string repo) +    { +        return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"; +    } + +    private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath) +    { +        using var responseStream = httpClient.GetStreamAsync(GetGithubRepositoryArchiveUrl(user, repo)).Result; +        using var outputFileStream = File.OpenWrite(outputPath); +        responseStream.CopyTo(outputFileStream); +    } + +    private static void Unzip(string zipPath, string outputPath) +    { +        using var zip = ZipFile.OpenRead(zipPath) ?? throw new Exception($"Failed to open zip file {zipPath}"); +        zip.ExtractToDirectory(outputPath); +    } + +    private string DownloadAndExtractGeoSiteRepository(bool silent) +    { +        const string zipFileName = "v2ray-geosite-master.zip"; +        using var httpClient = new HttpClient(); +        var tempDirectory = Directory.CreateTempSubdirectory(Program.Name); +        var archivePath = Path.Combine(tempDirectory.FullName, zipFileName); +        var extractPath = Path.Combine(tempDirectory.FullName, "repo"); +        GithubDownloadRepository(httpClient, V2rayGithubOrganization, V2rayGeoSiteGithubRepository, archivePath); +        Directory.CreateDirectory(extractPath); +        Unzip(archivePath, extractPath); +        return Path.Join(extractPath, "domain-list-community-master"); +    } +} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs index e06a92d..a40aa21 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs @@ -4,6 +4,8 @@ namespace Crupest.V2ray;  public static class Program  { +    public static string Name { get; } = typeof(Program).Namespace ?? throw new Exception("Can't get the name of Crupest.V2ray."); +      public static string CrupestV2rayDirectory { get; } =          Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ??          Path.GetFullPath(Path.GetDirectoryName( @@ -13,10 +15,31 @@ public static class Program      public static void RunV2rayAndWatchConfigChange()      { -        var v2rayPath = V2rayController.FindExecutable(CrupestV2rayDirectory) ?? +        var v2rayPath = V2rayController.FindExecutable(CrupestV2rayDirectory, out var isLocal) ??              throw new Exception("Can't find v2ray executable either in Crupest.V2ray directory or in PATH."); -        var v2rayController = new V2rayController(v2rayPath, Path.Combine(CrupestV2rayDirectory, ConfigOutputFileName), CrupestV2rayDirectory); +        string? assetsPath; +        if (isLocal) +        { +            assetsPath = CrupestV2rayDirectory; +            var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestV2rayDirectory, out var missing); +            if (!assetsComplete) +            { +                throw new Exception($"Missing assets: {string.Join(", ", missing)} in {CrupestV2rayDirectory}. This v2ray is local. So only use assets in Crupest.V2ray directory."); +            } +        } +        else +        { +            assetsPath = CrupestV2rayDirectory; +            var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestV2rayDirectory, out var missing); +            if (!assetsComplete) +            { +                Console.WriteLine($"Missing assets: {string.Join(", ", missing)} in {CrupestV2rayDirectory}. This v2ray is global. So fallback to its own assets."); +                assetsPath = null; +            } +        } + +        var v2rayController = new V2rayController(v2rayPath, Path.Combine(CrupestV2rayDirectory, ConfigOutputFileName), assetsPath);          var configFileWatcher = new FileWatcher(CrupestV2rayDirectory, V2rayConfig.ConfigFileNames);          V2rayConfig.FromDirectoryAndWriteToFile(CrupestV2rayDirectory, Path.Join(CrupestV2rayDirectory, ConfigOutputFileName)); @@ -42,8 +65,7 @@ public static class Program              var verb = args[0].ToLower();              if (verb == "download-geodata" || verb == "dg")              { -                var geoDataDownloader = new GeoDataDownloader(); -                geoDataDownloader.Download(CrupestV2rayDirectory); +                GeoDataManager.Instance.Download(CrupestV2rayDirectory, false);                  return;              }              else if (verb == "generate" || verb == "g") diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs index c246bb5..3758f47 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs @@ -42,6 +42,14 @@ public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouti      private const string RoutingAnchor = "ROUTING_ANCHOR";      private const string HostsAnchor = "HOSTS_ANCHOR"; +    public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V@RAY_GEOSITE_USE_CN"; + +    private static bool UseCnGeoSite => Environment.GetEnvironmentVariable(AddCnAttributeToGeositeEnvironmentVariable) switch +    { +        "0" or "false" or "off" or "disable" => false, +        _ => true +    }; +      public Template Template { get; set; } = template;      public List<V2rayProxy> Proxies { get; set; } = proxies;      public V2rayRouting Routing { get; set; } = router; @@ -118,7 +126,7 @@ public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouti              file = vmessPath;              var vmess = V2rayVmessProxy.CreateFromConfigString(vmessString, "proxy");              file = proxyPath; -            var routing = V2rayRouting.CreateFromConfigString(routingString, "proxy"); +            var routing = V2rayRouting.CreateFromConfigString(routingString, "proxy", UseCnGeoSite);              file = hostsPath ?? "";              var hosts = hostsString is not null ? V2rayHosts.CreateFromConfigString(hostsString) : null;              return new V2rayConfig(template, [vmess], routing, hosts); diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs index ab1614a..4656216 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs @@ -2,12 +2,13 @@ using System.Diagnostics;  namespace Crupest.V2ray; -public class V2rayController(string executablePath, string configPath, string assetPath) +public class V2rayController(string executablePath, string configPath, string? assetPath)  {      public const string V2rayAssetEnvironmentVariableName = "v2ray.location.asset"; -    public static string? FindExecutable(string contentDir, string? executableName = null) +    public static string? FindExecutable(string contentDir, out bool isLocal, string? executableName = null)      { +        isLocal = false;          executableName ??= "v2ray";          if (OperatingSystem.IsWindows()) @@ -18,6 +19,7 @@ public class V2rayController(string executablePath, string configPath, string as          var localV2rayPath = Path.Combine(contentDir, executableName);          if (File.Exists(localV2rayPath))          { +            isLocal = true;              return localV2rayPath;          } @@ -39,7 +41,7 @@ public class V2rayController(string executablePath, string configPath, string as      public string ExecutablePath { get; } = executablePath;      public string ConfigPath { get; } = configPath; -    public string AssetPath { get; } = assetPath; +    public string? AssetPath { get; } = assetPath;      public Process? CurrentProcess { get; private set; }      private Process CreateProcess() @@ -53,7 +55,10 @@ public class V2rayController(string executablePath, string configPath, string as          startInfo.ArgumentList.Add("run");          startInfo.ArgumentList.Add("-c");          startInfo.ArgumentList.Add(ConfigPath); -        startInfo.EnvironmentVariables[V2rayAssetEnvironmentVariableName] = AssetPath; +        if (AssetPath is not null) +        { +            startInfo.EnvironmentVariables[V2rayAssetEnvironmentVariableName] = AssetPath; +        }          process.StartInfo = startInfo;          process.OutputDataReceived += (_, args) => diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs index 528b49e..080a7b4 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs @@ -17,9 +17,12 @@ public record V2rayRoutingRule(V2rayHostMatcherKind MatcherKind, string MatcherS      public static Dictionary<string, List<V2rayRoutingRule>> GroupByOutboundTag(List<V2rayRoutingRule> rules)          => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); -    public static Dictionary<V2rayHostMatcherKind, List<V2rayRoutingRule>> GroupByMatcherByKind(List<V2rayRoutingRule> rules) +    public static Dictionary<V2rayHostMatcherKind, List<V2rayRoutingRule>> GroupByMatcherKind(List<V2rayRoutingRule> rules)          => rules.GroupBy(r => r.MatcherKind).Select(g => (g.Key, g.ToList())).ToDictionary(); +    public static List<List<V2rayRoutingRule>> GroupByOutboundTagAndMatcherKind(List<V2rayRoutingRule> rules) +        => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatcherKind(groupByTag).Values).ToList(); +      public static V2rayV4ConfigJsonObjects.RoutingRule ListToJsonObject(List<V2rayRoutingRule> rules)      {          if (rules.Count == 0) @@ -43,30 +46,48 @@ public record V2rayRoutingRule(V2rayHostMatcherKind MatcherKind, string MatcherS          );      } +    public V2rayRoutingRule CloneGeositeWithCnAttribute(string outboundTag) +    { +        if (MatcherKind is not V2rayHostMatcherKind.GeoSite) +        { +            throw new ArgumentException("Matcher kind must be GeoSite."); +        } + +        return new V2rayRoutingRule(V2rayHostMatcherKind.GeoSite, $"{MatcherString}@cn", outboundTag); +    } +      public V2rayV4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]);      object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();  } -public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand") : IV2rayV4ConfigObject +public record V2rayRouting(List<V2rayRoutingRule> Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV2rayV4ConfigObject  { -    public V2rayV4ConfigJsonObjects.Routing ToJsonObjectV4() +    public List<V2rayRoutingRule> CreateGeositeCnDirectRules()      { -        var ruleJsonObjects = new List<object>(); +        return Rules.Where(r => r.MatcherKind is V2rayHostMatcherKind.GeoSite) +            .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList(); +    } + +    public V2rayV4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true) +    { +        List<V2rayV4ConfigJsonObjects.RoutingRule> ruleJsonObjects = []; + +        if (directGeositeCn) +        { +            ruleJsonObjects.Add(V2rayRoutingRule.ListToJsonObject(CreateGeositeCnDirectRules())); +        } -        var rules = V2rayRoutingRule.GroupByOutboundTag(Rules).ToList().SelectMany((groupByTag) => -            V2rayRoutingRule.GroupByMatcherByKind(groupByTag.Value).ToList().Select((groupByMatcher) => -                V2rayRoutingRule.ListToJsonObject(groupByMatcher.Value)) -        ).ToList(); +        ruleJsonObjects.AddRange(V2rayRoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(V2rayRoutingRule.ListToJsonObject)); -        return new V2rayV4ConfigJsonObjects.Routing(rules); +        return new V2rayV4ConfigJsonObjects.Routing(ruleJsonObjects);      }      object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); -    public static V2rayRouting CreateFromConfigString(string configString, string outboundTag) +    public static V2rayRouting CreateFromConfigString(string configString, string outboundTag, bool directGeositeCn = true)      {          var matcherConfig = new V2rayHostMatcherConfig(configString, [.. Enum.GetValues<V2rayHostMatcherKind>()], maxComponentCount: 0); -        return new V2rayRouting(matcherConfig.Items.Select(i => new V2rayRoutingRule(i.Kind, i.Matcher, outboundTag)).ToList()); +        return new V2rayRouting(matcherConfig.Items.Select(i => new V2rayRoutingRule(i.Kind, i.Matcher, outboundTag)).ToList(), directGeositeCn);      }  } diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template index 424e996..686006c 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template +++ b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template @@ -4,7 +4,7 @@    },    "inbounds": [      { -      "port": 2081, +      "port": 3081,        "listen": "127.0.0.1",        "tag": "socks-inbound",        "protocol": "socks", @@ -13,7 +13,7 @@        }      },      { -      "port": 2080, +      "port": 3080,        "listen": "127.0.0.1",        "tag": "http-inbound",        "protocol": "http", | 
