aboutsummaryrefslogtreecommitdiff
path: root/tools/Crupest.V2ray
diff options
context:
space:
mode:
Diffstat (limited to 'tools/Crupest.V2ray')
-rw-r--r--tools/Crupest.V2ray/.gitignore5
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/.gitignore1
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs55
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj22
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs94
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs42
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Program.cs45
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml13
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs49
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs88
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs52
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs25
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs74
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs54
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/config.json.template63
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/proxy.txt16
-rw-r--r--tools/Crupest.V2ray/CrupestV2ray.sln33
17 files changed, 731 insertions, 0 deletions
diff --git a/tools/Crupest.V2ray/.gitignore b/tools/Crupest.V2ray/.gitignore
new file mode 100644
index 0000000..15db15d
--- /dev/null
+++ b/tools/Crupest.V2ray/.gitignore
@@ -0,0 +1,5 @@
+.vs
+bin
+obj
+*.pubxml.user
+*.csproj.user
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/.gitignore b/tools/Crupest.V2ray/Crupest.V2ray/.gitignore
new file mode 100644
index 0000000..c936492
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/.gitignore
@@ -0,0 +1 @@
+vmess.txt
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs
new file mode 100644
index 0000000..9ca5741
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs
@@ -0,0 +1,55 @@
+namespace Crupest.V2ray;
+
+public class ConfigGenerationWatcher
+{
+ public ConfigGenerationWatcher() : this(Program.ExeDir, Program.ConfigTemplateFileName, Program.VmessConfigFileName, Program.ProxyConfigFileName, Path.Combine(Program.ExeDir, Program.ConfigOutputFileName), new List<string>())
+ {
+
+ }
+
+ public ConfigGenerationWatcher(string directory, string configTemplateFileName, string vmessConfigFileName, string proxyConfigFileName, string configOutputPath, List<string> otherWatchFiles)
+ {
+ Directory = directory;
+ ConfigTemplateFileName = configTemplateFileName;
+ VmessConfigFileName = vmessConfigFileName;
+ ProxyConfigFileName = proxyConfigFileName;
+ ConfigOutputPath = configOutputPath;
+ OtherWatchFiles = otherWatchFiles;
+ }
+
+ public string Directory { get; set; }
+ public string ConfigTemplateFileName { get; set; }
+ public string VmessConfigFileName { get; set; }
+ public string ProxyConfigFileName { get; set; }
+ public List<string> OtherWatchFiles { get; set; }
+ public string ConfigOutputPath { get; set; }
+
+ public string ConfigTemplateFilePath => Path.Combine(Directory, ConfigTemplateFileName);
+ public string VmessConfigFilePath => Path.Combine(Directory, VmessConfigFileName);
+ public string ProxyConfigFilePath => Path.Combine(Directory, ProxyConfigFileName);
+
+ public delegate void OnConfigChangedHandler();
+
+ public void Generate()
+ {
+ var config = V2rayConfig.FromFiles(ConfigTemplateFilePath, VmessConfigFilePath, ProxyConfigFilePath);
+
+ File.WriteAllText(ConfigOutputPath, config.ToJson());
+ }
+
+ public void Run(OnConfigChangedHandler onChanged)
+ {
+ var sourceWatcher = new FileSystemWatcher(Directory);
+ sourceWatcher.Filters.Add(ConfigTemplateFileName);
+ sourceWatcher.Filters.Add(VmessConfigFileName);
+ sourceWatcher.Filters.Add(ProxyConfigFileName);
+ OtherWatchFiles.ForEach((f) => sourceWatcher.Filters.Add(f));
+ sourceWatcher.NotifyFilter = NotifyFilters.LastWrite;
+
+ while (true)
+ {
+ var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed);
+ onChanged();
+ }
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj
new file mode 100644
index 0000000..38f0937
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Update="config.json.template">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Update="proxy.txt">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ <None Update="vmess.txt">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+</Project>
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs
new file mode 100644
index 0000000..08de673
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs
@@ -0,0 +1,94 @@
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Crupest.V2ray;
+
+public static partial class FileUtility
+{
+ public static List<string> ReadList(string str)
+ {
+ return str.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
+ }
+
+ public static Dictionary<string, string> ReadDictionary(string str, bool keyToLower = true)
+ {
+ var lines = str.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ var result = new Dictionary<string, string>();
+ for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++)
+ {
+ var line = lines[lineNumber];
+ if (!line.Contains('='))
+ {
+ throw new FormatException($"Line {lineNumber + 1} does not contain a '='.");
+ }
+ var equalIndex = line.IndexOf('=');
+ var key = line[..equalIndex].Trim();
+ if (keyToLower) key = key.ToLower();
+ var value = line[(equalIndex + 1)..].Trim();
+ result[key] = value;
+ }
+ return result;
+ }
+
+ public static List<string> ReadListFile(string path, bool required = true)
+ {
+ if (File.Exists(path))
+ {
+ return ReadList(File.ReadAllText(path));
+ }
+ else
+ {
+ if (required)
+ {
+ throw new FileNotFoundException($"File {path} is required but it does not exist.");
+ }
+ return new();
+ }
+ }
+
+ public static Dictionary<string, string> ReadDictionaryFile(string path, bool required = true, bool keyToLower = true)
+ {
+ if (File.Exists(path))
+ {
+ return ReadDictionary(File.ReadAllText(path), keyToLower);
+ }
+ else
+ {
+ if (required)
+ {
+ throw new FileNotFoundException($"File {path} is required but it does not exist.");
+ }
+ return new();
+ }
+ }
+
+ private static Regex TemplateValuePattern { get; } = CreateTemplateValuePattern();
+
+ [GeneratedRegex(@"\$\{\s*([_a-zA-Z][_a-zA-Z0-9]*)\s*\}")]
+ private static partial Regex CreateTemplateValuePattern();
+
+ public static string TextFromTemplate(string template, Dictionary<string, string> dict)
+ {
+ return TemplateValuePattern.Replace(template, (match) =>
+ {
+ var key = match.Groups[1].Value;
+ if (dict.ContainsKey(key))
+ {
+ return dict[key];
+ }
+ return match.Value;
+ });
+ }
+
+ public static string JsonFormat(string json)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ };
+
+ return JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(json, options), options);
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs
new file mode 100644
index 0000000..d3efc1f
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataDownloader.cs
@@ -0,0 +1,42 @@
+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/Program.cs b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs
new file mode 100644
index 0000000..793f6e7
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics;
+using System.Reflection;
+
+namespace Crupest.V2ray;
+
+public static class Program
+{
+ public const string ConfigTemplateFileName = "config.json.template";
+ public const string VmessConfigFileName = "vmess.txt";
+ public const string ProxyConfigFileName = "proxy.txt";
+ public const string ConfigOutputFileName = "config.json";
+
+ public static string ExeDir { get; } = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of exe."));
+
+
+ public static void Main(string[] args)
+ {
+ if (args.Length != 0)
+ {
+ if (args.Length != 1)
+ {
+ throw new Exception("Invalid command line arguments.");
+ }
+ var verb = args[0].ToLower();
+ if (verb == "download-geodata" || verb == "dg")
+ {
+ var geoDataDonwloader = new GeoDataDownloader();
+ geoDataDonwloader.Download(ExeDir);
+ return;
+ }
+ }
+
+ var v2rayController = new V2rayController();
+ var configGenerationWatcher = new ConfigGenerationWatcher();
+
+ configGenerationWatcher.Generate();
+ v2rayController.Start();
+
+ configGenerationWatcher.Run(() =>
+ {
+ configGenerationWatcher.Generate();
+ v2rayController.Restart();
+ });
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml b/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 0000000..bbdd2ad
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+https://go.microsoft.com/fwlink/?LinkID=208121.
+-->
+<Project>
+ <PropertyGroup>
+ <Configuration>Release</Configuration>
+ <Platform>Any CPU</Platform>
+ <PublishDir>bin\Release\net7.0\publish\</PublishDir>
+ <PublishProtocol>FileSystem</PublishProtocol>
+ <_TargetId>Folder</_TargetId>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs
new file mode 100644
index 0000000..82dc11c
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs
@@ -0,0 +1,49 @@
+using System.Text.Json;
+
+namespace Crupest.V2ray;
+
+public class V2rayConfig
+{
+ private const string VmessAnchor = "VMESS_PROXY_ANCHOR";
+ private const string RoutingAnchor = "ROUTING_ANCHOR";
+
+ public V2rayConfig(string template, V2rayVmessProxy vmess, V2rayRouting router)
+ {
+ Template = template;
+ Vmess = vmess;
+ Routing = router;
+ }
+
+ public string Template { get; set; }
+ public V2rayVmessProxy Vmess { get; set; }
+ public V2rayRouting Routing { get; set; }
+
+ public string ToJson(bool pretty = true)
+ {
+ var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ });
+
+ var templateValues = new Dictionary<string, string>
+ {
+ [VmessAnchor] = JsonSerializer.Serialize(Vmess.ToOutboundJsonObject(), jsonOptions),
+ [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObject(), jsonOptions)
+ };
+
+ return FileUtility.JsonFormat(FileUtility.TextFromTemplate(Template, templateValues));
+ }
+
+ public static V2rayConfig FromFiles(string templatePath, string vmessPath, string routingPath)
+ {
+ var template = File.ReadAllText(templatePath);
+ var vmessDict = FileUtility.ReadDictionaryFile(vmessPath);
+ var proxyRoutingList = FileUtility.ReadListFile(routingPath);
+
+ var vmess = V2rayVmessProxy.FromDictionary(vmessDict);
+ var routing = V2rayRouting.FromStringList(proxyRoutingList);
+
+ return new V2rayConfig(template, vmess, routing);
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs
new file mode 100644
index 0000000..5b8fcac
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs
@@ -0,0 +1,88 @@
+using System.Diagnostics;
+
+namespace Crupest.V2ray;
+
+public class V2rayController
+{
+ public const string V2RayAssetLocationEnvironmentVariableName = "V2RAY_LOCATION_ASSET";
+ public const string V2RayConfigLocationEnvironmentVariableName = "V2RAY_LOCATION_CONFIG";
+
+ public V2rayController(string v2rayExePath = "v2ray") : this(v2rayExePath, Program.ExeDir, Program.ExeDir)
+ {
+
+ }
+
+ public V2rayController(string v2rayExePath, string configDirPath, string assetDirPath)
+ {
+ V2rayExePath = v2rayExePath;
+ ConfigDirPath = configDirPath;
+ AssetDirPath = assetDirPath;
+ }
+
+ public string V2rayExePath { get; }
+ public string ConfigDirPath { get; }
+ public string AssetDirPath { get; }
+ public Process? CurrentProcess { get; private set; }
+
+ private Process CreateProcess()
+ {
+ var process = new Process();
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = V2rayExePath,
+ };
+ startInfo.EnvironmentVariables[V2RayConfigLocationEnvironmentVariableName] = ConfigDirPath;
+ startInfo.EnvironmentVariables[V2RayAssetLocationEnvironmentVariableName] = AssetDirPath;
+
+ process.StartInfo = startInfo;
+ process.OutputDataReceived += (_, args) =>
+ {
+ Console.Out.Write(args.Data);
+ };
+ process.ErrorDataReceived += (_, args) =>
+ {
+ Console.Error.WriteLine(args.Data);
+ };
+
+ return process;
+ }
+
+ public void Stop()
+ {
+ if (CurrentProcess is not null)
+ {
+ CurrentProcess.Kill();
+ CurrentProcess.Dispose();
+ CurrentProcess = null;
+ Console.WriteLine("V2ray stopped.");
+ }
+ }
+
+ public void Start(bool stopOld = false)
+ {
+ if (stopOld) Stop();
+
+ if (CurrentProcess is null)
+ {
+ CurrentProcess = CreateProcess();
+ CurrentProcess.EnableRaisingEvents = true;
+ CurrentProcess.Exited += (_, _) =>
+ {
+ if (CurrentProcess.ExitCode != 0)
+ {
+ const string message = "V2ray exits with error.";
+ Console.Error.WriteLine(message);
+ throw new Exception(message);
+ }
+ };
+ CurrentProcess.Start();
+ Console.WriteLine("V2ray started.");
+ }
+ }
+
+ public void Restart()
+ {
+ Start(true);
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs
new file mode 100644
index 0000000..7265b09
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs
@@ -0,0 +1,52 @@
+namespace Crupest.V2ray;
+
+public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand")
+{
+ public record DomainRuleJsonObject(List<string> Domains, string OutboundTag, string Type = "field");
+
+ public record IpRuleJsonObject(List<string> Ip, string OutboundTag, string Type = "field");
+
+ public record RoutingJsonObject(string DomainStrategy, List<object> Rules);
+
+ public V2rayRouting() : this(new List<V2rayRoutingRule>())
+ {
+
+ }
+
+ public RoutingJsonObject ToJsonObject()
+ {
+ var ruleJsonObjects = new List<object>();
+
+ foreach (var (outBoundTag, proxyRules) in V2rayRoutingRule.GroupByOutboundTag(Rules))
+ {
+ foreach (var (matchByKind, rules) in V2rayRoutingRule.GroupByMatchByKind(proxyRules))
+ {
+ ruleJsonObjects.Add(
+ matchByKind switch
+ {
+ V2rayRoutingRuleMatcher.MatchByKind.Ip => new IpRuleJsonObject(rules.Select(r => r.Matcher.ToString()).ToList(), outBoundTag),
+ V2rayRoutingRuleMatcher.MatchByKind.Domain => new DomainRuleJsonObject(rules.Select(r => r.Matcher.ToString()).ToList(), outBoundTag),
+ _ => throw new Exception("Unknown match by kind."),
+ }
+ );
+ }
+ }
+
+ return new RoutingJsonObject(DomainStrategy, ruleJsonObjects);
+ }
+
+ public static V2rayRouting FromStringList(List<string> list, string outboundTag = "proxy")
+ {
+ var router = new V2rayRouting();
+
+ foreach (var line in list)
+ {
+ var matcher = V2rayRoutingRuleMatcher.Parse(line);
+ if (matcher != null)
+ router.Rules.Add(new V2rayRoutingRule(matcher, outboundTag));
+ }
+
+ return router;
+ }
+}
+
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs
new file mode 100644
index 0000000..0218183
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs
@@ -0,0 +1,25 @@
+namespace Crupest.V2ray;
+
+public record V2rayRoutingRule(V2rayRoutingRuleMatcher Matcher, string OutboundTag)
+{
+ public static Dictionary<string, List<V2rayRoutingRule>> GroupByOutboundTag(List<V2rayRoutingRule> rules)
+ {
+ var result = new Dictionary<string, List<V2rayRoutingRule>>();
+ foreach (var group in rules.GroupBy(r => r.OutboundTag))
+ {
+ result[group.Key] = group.ToList();
+ }
+ return result;
+ }
+
+ public static Dictionary<V2rayRoutingRuleMatcher.MatchByKind, List<V2rayRoutingRule>> GroupByMatchByKind(List<V2rayRoutingRule> rules)
+ {
+ var result = new Dictionary<V2rayRoutingRuleMatcher.MatchByKind, List<V2rayRoutingRule>>();
+ foreach (var group in rules.GroupBy(r => r.Matcher.MatchBy))
+ {
+ result[group.Key] = group.ToList();
+ }
+ return result;
+ }
+}
+
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs
new file mode 100644
index 0000000..a13237c
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs
@@ -0,0 +1,74 @@
+namespace Crupest.V2ray;
+
+public record V2rayRoutingRuleMatcher(V2rayRoutingRuleMatcher.MatchKind Kind, string Value)
+{
+ public enum MatchByKind
+ {
+ Domain,
+ Ip
+ }
+
+ public enum MatchKind
+ {
+ GeoIp,
+ GeoSite,
+ DomainPlain,
+ DomainSuffix,
+ DomainRegex,
+ DomainFull,
+ }
+
+ public MatchByKind MatchBy
+ {
+ get
+ {
+ return Kind switch
+ {
+ MatchKind.GeoIp => MatchByKind.Ip,
+ _ => MatchByKind.Domain
+ };
+ }
+ }
+
+ public static V2rayRoutingRuleMatcher? Parse(string line)
+ {
+ if (line.IndexOf('#') != -1)
+ {
+ line = line[..line.IndexOf('#')];
+ }
+
+ line = line.Trim();
+
+ if (line.Length == 0) { return null; }
+
+ var kind = MatchKind.DomainSuffix;
+
+ foreach (var name in Enum.GetNames<MatchKind>())
+ {
+ if (line.StartsWith(name))
+ {
+ kind = Enum.Parse<MatchKind>(name);
+ line = line[name.Length..];
+ line = line.Trim();
+ break;
+ }
+ }
+
+ return new V2rayRoutingRuleMatcher(kind, line);
+ }
+
+
+ public override string ToString()
+ {
+ return Kind switch
+ {
+ MatchKind.GeoSite => $"geosite:{Value}",
+ MatchKind.GeoIp => $"geoip:{Value}",
+ MatchKind.DomainPlain => Value,
+ MatchKind.DomainSuffix => $"domain:{Value}",
+ MatchKind.DomainFull => $"full:{Value}",
+ MatchKind.DomainRegex => $"regexp:{Value}",
+ _ => throw new Exception("Unknown matcher kind."),
+ };
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs
new file mode 100644
index 0000000..e701463
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs
@@ -0,0 +1,54 @@
+namespace Crupest.V2ray;
+
+public class V2rayVmessProxy
+{
+ public record VmessOutboundJsonObject(string Protocol, SettingsJsonObject Settings, string Tag, StreamSettingsJsonObject StreamSettings)
+ {
+ public static VmessOutboundJsonObject ByWs(string address, int port, string uuid, string tag, string path)
+ {
+ return new VmessOutboundJsonObject("vmess", new SettingsJsonObject(
+ new List<VnextJsonObject> { new VnextJsonObject(address, port, new List<VnextUserJsonObject> { new VnextUserJsonObject(uuid) }) }
+ ), tag, StreamSettingsJsonObject.Ws(path));
+ }
+ }
+
+ public record SettingsJsonObject(List<VnextJsonObject> Vnext);
+
+ public record VnextJsonObject(string Address, int Port, List<VnextUserJsonObject> Users);
+
+ public record VnextUserJsonObject(string Id, int AlterId = 0, string Security = "auto", int Level = 0);
+
+ public record StreamSettingsJsonObject(string Network, string Security, WsSettingsJsonObject WsSettings)
+ {
+ public static StreamSettingsJsonObject Ws(string path)
+ {
+ return new StreamSettingsJsonObject("ws", "tls", new WsSettingsJsonObject(path, new()));
+ }
+ }
+
+ public record WsSettingsJsonObject(string Path, Dictionary<string, string> Headers);
+
+ public string Host { get; set; }
+ public int Port { get; set; }
+ public string Path { get; set; }
+ public string UserId { get; set; }
+
+
+ public V2rayVmessProxy(string host, int port, string userId, string path)
+ {
+ Host = host;
+ Port = port;
+ UserId = userId;
+ Path = path;
+ }
+
+ public VmessOutboundJsonObject ToOutboundJsonObject(string tag = "proxy")
+ {
+ return VmessOutboundJsonObject.ByWs(Host, Port, UserId, tag, Path);
+ }
+
+ public static V2rayVmessProxy FromDictionary(Dictionary<string, string> dict)
+ {
+ return new V2rayVmessProxy(dict["host"], int.Parse(dict["port"]), dict["userid"], dict["path"]);
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template
new file mode 100644
index 0000000..47187bb
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template
@@ -0,0 +1,63 @@
+{
+ "log": {
+ "loglevel": "info"
+ },
+ "inbounds": [
+ {
+ "port": 2081,
+ "listen": "127.0.0.1",
+ "tag": "socks-inbound",
+ "protocol": "socks",
+ "settings": {
+ "auth": "noauth"
+ }
+ },
+ {
+ "port": 2080,
+ "listen": "127.0.0.1",
+ "tag": "http-inbound",
+ "protocol": "http",
+ "settings": {
+ "auth": "noauth"
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "settings": {},
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "settings": {},
+ "tag": "blocked"
+ },
+ ${VMESS_PROXY_ANCHOR}
+ ],
+ "routing": ${ROUTING_ANCHOR},
+ "dns": {
+ "hosts": {},
+ "servers": [
+ "https://doh.pub/dns-query",
+ "1.1.1.1",
+ "8.8.8.8",
+ "localhost"
+ ]
+ },
+ "policy": {
+ "levels": {
+ "0": {
+ "uplinkOnly": 0,
+ "downlinkOnly": 0
+ }
+ },
+ "system": {
+ "statsInboundUplink": false,
+ "statsInboundDownlink": false,
+ "statsOutboundUplink": false,
+ "statsOutboundDownlink": false
+ }
+ },
+ "other": {}
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt b/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt
new file mode 100644
index 0000000..4e9d3e8
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt
@@ -0,0 +1,16 @@
+GeoSite github
+GeoSite google
+GeoSite youtube
+GeoSite twitter
+GeoSite facebook
+GeoSite discord
+GeoSite reddit
+GeoSite wikimedia
+GeoSite stackexchange
+GeoSite libgen
+GeoSite python
+GeoSite ruby
+GeoSite creativecommons
+GeoSite sci-hub
+GeoSite v2ray
+GeoSite imgur
diff --git a/tools/Crupest.V2ray/CrupestV2ray.sln b/tools/Crupest.V2ray/CrupestV2ray.sln
new file mode 100644
index 0000000..3045b4e
--- /dev/null
+++ b/tools/Crupest.V2ray/CrupestV2ray.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.7.34024.191
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}"
+ ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crupest.V2ray", "Crupest.V2ray\Crupest.V2ray.csproj", "{154D49F2-242E-4384-8D34-73774231AA75}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {154D49F2-242E-4384-8D34-73774231AA75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {154D49F2-242E-4384-8D34-73774231AA75}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {154D49F2-242E-4384-8D34-73774231AA75}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {154D49F2-242E-4384-8D34-73774231AA75}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {154D49F2-242E-4384-8D34-73774231AA75} = {F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B1E8FD9C-9157-4F4E-8265-4B37F30EEC5E}
+ EndGlobalSection
+EndGlobal