aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2024-09-17 23:59:44 +0800
committercrupest <crupest@outlook.com>2024-09-18 21:47:50 +0800
commit57514635a10bad1feceda4c6f576baf07607af33 (patch)
treed08be10b64947caa033bd1b67b1417fe99ae462a
parentcbe3f1de55795809ddda910122bc51c395dae48c (diff)
downloadcrupest-57514635a10bad1feceda4c6f576baf07607af33.tar.gz
crupest-57514635a10bad1feceda4c6f576baf07607af33.tar.bz2
crupest-57514635a10bad1feceda4c6f576baf07607af33.zip
refactor(secret): refactor codes and make config work.
-rw-r--r--crupest-words.txt4
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Config.cs87
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs58
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj2
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs94
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs26
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs7
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs7
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Program.cs50
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/Template.cs231
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs146
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs81
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs95
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs19
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs48
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs41
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs58
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs97
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs35
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs142
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs21
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs20
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs25
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs (renamed from tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigObjects.cs)2
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs58
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs64
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/config.json.template2
-rw-r--r--tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template4
28 files changed, 847 insertions, 677 deletions
diff --git a/crupest-words.txt b/crupest-words.txt
index af85fa2..62906cb 100644
--- a/crupest-words.txt
+++ b/crupest-words.txt
@@ -1,9 +1,13 @@
crupest
# secret
+vnext
vmess
confdir
geodata
+geosite
+geoip
+userid
# university
ustc
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs
new file mode 100644
index 0000000..fc71007
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs
@@ -0,0 +1,87 @@
+namespace Crupest.V2ray;
+
+public record ConfigItem(string Value, int LineNumber);
+
+public class DictionaryConfig(string configString, List<string>? requiredKeys = null)
+{
+ private static Dictionary<string, ConfigItem> Parse(string configString, List<string>? requiredKeys = null)
+ {
+ var config = new Dictionary<string, ConfigItem>();
+ var lines = configString.Split('\n');
+ int lineNumber = 1;
+
+ foreach (var line in lines)
+ {
+ var trimmedLine = line.Trim();
+ if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith('#'))
+ {
+ lineNumber++;
+ continue;
+ }
+
+ var equalIndex = trimmedLine.IndexOf('=');
+ if (equalIndex == -1)
+ {
+ throw new FormatException($"No '=' found in line {lineNumber}.");
+ }
+
+ config.Add(trimmedLine[..equalIndex].Trim(), new ConfigItem(trimmedLine[(equalIndex + 1)..].Trim(), lineNumber));
+ lineNumber++;
+ }
+
+ if (requiredKeys is not null)
+ {
+ foreach (var key in requiredKeys)
+ {
+ if (!config.ContainsKey(key))
+ {
+ throw new FormatException($"Required key '{key}' not found in config.");
+ }
+ }
+ }
+
+ return config;
+ }
+
+ public string ConfigString { get; } = configString;
+ public List<string>? RequiredKeys { get; } = requiredKeys;
+ public Dictionary<string, ConfigItem> Config { get; } = Parse(configString);
+ public ConfigItem GetItemCaseInsensitive(string key)
+ {
+ foreach (var (originalKey, value) in Config)
+ {
+ if (string.Equals(originalKey, key, StringComparison.OrdinalIgnoreCase))
+ {
+ return value;
+ }
+ }
+ throw new KeyNotFoundException($"Key '{key}' not found in config case-insensitively.");
+ }
+}
+
+public class ListConfig(string configString)
+{
+ private static List<ConfigItem> Parse(string configString)
+ {
+ var config = new List<ConfigItem>();
+ var lines = configString.Split('\n');
+ int lineNumber = 1;
+
+ foreach (var line in lines)
+ {
+ var trimmedLine = line.Trim();
+ if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith('#'))
+ {
+ lineNumber++;
+ continue;
+ }
+ config.Add(new ConfigItem(trimmedLine, lineNumber));
+ lineNumber++;
+ }
+
+ return config;
+ }
+
+ public string ConfigString { get; } = configString;
+ public List<ConfigItem> Config { get; } = Parse(configString);
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs
deleted file mode 100644
index 32be5b0..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace Crupest.V2ray;
-
-public class ConfigGenerationWatcher
-{
- public ConfigGenerationWatcher() : this(Program.ExeDir, Program.ConfigTemplateFileName, Program.VmessConfigFileName, Program.ProxyConfigFileName, Program.HostsConfigFileName, Path.Combine(Program.ExeDir, Program.ConfigOutputFileName), new List<string>())
- {
-
- }
-
- public ConfigGenerationWatcher(string directory, string configTemplateFileName, string vmessConfigFileName, string proxyConfigFileName, string hostsConfigFileName, string configOutputPath, List<string> otherWatchFiles)
- {
- Directory = directory;
- ConfigTemplateFileName = configTemplateFileName;
- VmessConfigFileName = vmessConfigFileName;
- ProxyConfigFileName = proxyConfigFileName;
- HostsConfigFileName = hostsConfigFileName;
- 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 string HostsConfigFileName { 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 string HostsConfigFilePath => Path.Combine(Directory, HostsConfigFileName);
-
- public delegate void OnConfigChangedHandler();
-
- public void Generate()
- {
- var config = V2rayConfig.FromFiles(ConfigTemplateFilePath, VmessConfigFilePath, ProxyConfigFilePath, HostsConfigFilePath);
-
- 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
index 3962fe4..0812e4c 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs
deleted file mode 100644
index 08de673..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-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/FileWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs
new file mode 100644
index 0000000..547adeb
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs
@@ -0,0 +1,26 @@
+namespace Crupest.V2ray;
+
+public class FileWatcher(string directory, List<string> fileNames)
+{
+ public string Directory { get; set; } = directory;
+ public List<string> FileNames { get; set; } = fileNames;
+
+ public delegate void OnChangedHandler();
+ public event OnChangedHandler? OnChanged;
+
+ public void Run()
+ {
+ var sourceWatcher = new FileSystemWatcher(Directory);
+ foreach (var fileName in FileNames)
+ {
+ sourceWatcher.Filters.Add(fileName);
+ }
+ sourceWatcher.NotifyFilter = NotifyFilters.LastWrite;
+
+ while (true)
+ {
+ var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed);
+ OnChanged?.Invoke();
+ }
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs
deleted file mode 100644
index 7643a01..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Crupest.V2ray;
-
-public interface IV2rayProxy
-{
- object ToOutboundJsonObject();
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs
deleted file mode 100644
index ae5fe1c..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Crupest.V2ray;
-
-public interface IV2rayStaticHostResolveResult
-{
- IDictionary<string, object> GetJsonProperties();
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs
index e623a88..e06a92d 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs
@@ -4,14 +4,32 @@ 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 HostsConfigFileName = "hosts.txt";
- public const string ConfigOutputFileName = "config.json";
+ public static string CrupestV2rayDirectory { get; } =
+ Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ??
+ Path.GetFullPath(Path.GetDirectoryName(
+ Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of Crupest.V2ray."));
- public static string ExeDir { get; } = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of exe."));
+ private const string ConfigOutputFileName = "config.json";
+ public static void RunV2rayAndWatchConfigChange()
+ {
+ var v2rayPath = V2rayController.FindExecutable(CrupestV2rayDirectory) ??
+ 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);
+ var configFileWatcher = new FileWatcher(CrupestV2rayDirectory, V2rayConfig.ConfigFileNames);
+
+ V2rayConfig.FromDirectoryAndWriteToFile(CrupestV2rayDirectory, Path.Join(CrupestV2rayDirectory, ConfigOutputFileName));
+ v2rayController.Start();
+
+ configFileWatcher.OnChanged += () =>
+ {
+ V2rayConfig.FromDirectoryAndWriteToFile(CrupestV2rayDirectory, Path.Join(CrupestV2rayDirectory, ConfigOutputFileName));
+ v2rayController.Restart();
+ };
+
+ configFileWatcher.Run();
+ }
public static void Main(string[] args)
{
@@ -25,22 +43,18 @@ public static class Program
if (verb == "download-geodata" || verb == "dg")
{
var geoDataDownloader = new GeoDataDownloader();
- geoDataDownloader.Download(ExeDir);
+ geoDataDownloader.Download(CrupestV2rayDirectory);
+ return;
+ }
+ else if (verb == "generate" || verb == "g")
+ {
+ var config = V2rayConfig.FromDirectory(CrupestV2rayDirectory);
+ Console.Out.WriteLine(config.ToJsonStringV4());
return;
}
throw new Exception("Invalid command line arguments.");
}
- var v2rayController = new V2rayController();
- var configGenerationWatcher = new ConfigGenerationWatcher();
-
- configGenerationWatcher.Generate();
- v2rayController.Start();
-
- configGenerationWatcher.Run(() =>
- {
- configGenerationWatcher.Generate();
- v2rayController.Restart();
- });
+ RunV2rayAndWatchConfigChange();
}
}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Template.cs b/tools/Crupest.V2ray/Crupest.V2ray/Template.cs
new file mode 100644
index 0000000..9c137b0
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/Template.cs
@@ -0,0 +1,231 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace Crupest.V2ray;
+
+public class Template
+{
+ private enum ParseState
+ {
+ Text,
+ Dollar,
+ LeftBracket,
+ VariableName,
+ VariableNameFinish,
+ }
+
+ private interface ITemplateNode
+ {
+ string Render(Dictionary<string, string> values);
+ }
+
+ private class TextNode(string text) : ITemplateNode
+ {
+
+ public string Text { get; } = text;
+
+ public string Render(Dictionary<string, string> values)
+ {
+ return Text;
+ }
+ }
+
+ private class VariableNode(string variableName) : ITemplateNode
+ {
+ public string VariableName { get; } = variableName;
+
+ public string Render(Dictionary<string, string> values)
+ {
+ return values.GetValueOrDefault(VariableName) ?? "";
+ }
+ }
+
+ public Template(string templateString)
+ {
+ TemplateString = templateString;
+ Nodes = Parse(templateString);
+ VariableNames = Nodes.OfType<VariableNode>().Select(node => node.VariableName).ToList();
+ }
+
+ private static List<ITemplateNode> Parse(string templateString)
+ {
+ int lineNumber = 1;
+ int columnNumber = 0;
+ List<ITemplateNode> nodes = [];
+ ParseState state = ParseState.Text;
+ StringBuilder stringBuilder = new();
+
+ string GetPosition() => $"line {lineNumber} column{columnNumber}";
+
+ [DoesNotReturn]
+ void ReportInvalidState(string message)
+ {
+ throw new Exception($"Invalid state at {GetPosition()}: {message}");
+ }
+
+ [DoesNotReturn]
+ void ReportInvalidCharacter(char c)
+ {
+ throw new FormatException($"Unexpected '{c}' at {GetPosition()}.");
+ }
+
+ void FinishText()
+ {
+ if (state != ParseState.Text)
+ {
+ ReportInvalidState($"Can't call FinishText here.");
+ }
+
+ if (stringBuilder.Length > 0)
+ {
+ nodes.Add(new TextNode(stringBuilder.ToString()));
+ stringBuilder.Clear();
+ }
+ }
+
+ foreach (var c in templateString)
+ {
+ if (c == '\n')
+ {
+ lineNumber++;
+ columnNumber = 0;
+ }
+
+ columnNumber++;
+
+ switch (c)
+ {
+ case '$':
+ if (state == ParseState.Text)
+ {
+ FinishText();
+ state = ParseState.Dollar;
+ }
+ else if (state == ParseState.Dollar)
+ {
+ if (stringBuilder.Length > 0)
+ {
+ throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty.");
+ }
+ stringBuilder.Append(c);
+ state = ParseState.Text;
+ }
+ else
+ {
+ throw new FormatException($"Unexpected '$' at {GetPosition()}.");
+ }
+ break;
+ case '{':
+ if (state == ParseState.Text)
+ {
+ stringBuilder.Append(c);
+ }
+ else if (state == ParseState.Dollar)
+ {
+ state = ParseState.LeftBracket;
+ }
+ else
+ {
+ throw new Exception($"Unexpected '{{' at {GetPosition()}.");
+ }
+ break;
+ case '}':
+ if (state == ParseState.Text)
+ {
+ stringBuilder.Append(c);
+ state = ParseState.Text;
+ }
+ else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish)
+ {
+ nodes.Add(new VariableNode(stringBuilder.ToString()));
+ stringBuilder.Clear();
+ state = ParseState.Text;
+ }
+ else
+ {
+ ReportInvalidCharacter(c);
+ }
+ break;
+ default:
+ if (state == ParseState.Dollar)
+ {
+ ReportInvalidCharacter(c);
+ }
+
+ if (char.IsWhiteSpace(c))
+ {
+ if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish)
+ {
+ continue;
+ }
+ else if (state == ParseState.Text)
+ {
+ stringBuilder.Append(c);
+ }
+ else if (state == ParseState.VariableName)
+ {
+ state = ParseState.VariableNameFinish;
+ }
+ else
+ {
+ ReportInvalidCharacter(c);
+ }
+ }
+ else
+ {
+ if (state == ParseState.Text)
+ {
+ stringBuilder.Append(c);
+ }
+ else if (state == ParseState.LeftBracket || state == ParseState.VariableName)
+ {
+ stringBuilder.Append(c);
+ state = ParseState.VariableName;
+ }
+ else
+ {
+ ReportInvalidCharacter(c);
+ }
+ }
+ break;
+ }
+ }
+
+ if (state == ParseState.Text)
+ {
+ FinishText();
+ }
+ else
+ {
+ throw new FormatException("Unexpected end of template string.");
+ }
+
+ return nodes;
+ }
+
+ public string TemplateString { get; }
+ private List<ITemplateNode> Nodes { get; set; }
+ public List<string> VariableNames { get; }
+
+ public string Generate(Dictionary<string, string> values, bool allowMissingVariable = false)
+ {
+ StringBuilder stringBuilder = new();
+ foreach (var node in Nodes)
+ {
+ if (node is TextNode textNode)
+ {
+ stringBuilder.Append(textNode.Text);
+ }
+ else if (node is VariableNode variableNode)
+ {
+ var hasValue = values.TryGetValue(variableNode.VariableName, out var value);
+ if (!hasValue && !allowMissingVariable)
+ {
+ throw new Exception($"Variable '{variableNode.VariableName}' is not set.");
+ }
+ stringBuilder.Append(hasValue ? value : string.Empty);
+ }
+ }
+ return stringBuilder.ToString();
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs
index 5623a20..c246bb5 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs
@@ -1,57 +1,147 @@
using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Crupest.V2ray;
-public class V2rayConfig
+public interface IV2rayV4ConfigObject
{
- private const string ProxyAnchor = "PROXY_ANCHOR";
- private const string VmessAnchor = "VMESS_PROXY_ANCHOR";
- private const string RoutingAnchor = "ROUTING_ANCHOR";
- private const string HostsAnchor = "HOSTS_ANCHOR";
+ object ToJsonObjectV4();
+}
- public V2rayConfig(string template, IV2rayProxy proxy, V2rayRouting router, V2rayHosts hosts)
+public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouting router, V2rayHosts? hosts)
+{
+ private class JsonInterfaceConverter<Interface> : JsonConverter<Interface>
{
- Template = template;
- Proxy = proxy;
- Routing = router;
- Hosts = hosts;
+ public override Interface Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Write(
+ Utf8JsonWriter writer,
+ Interface value,
+ JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, typeof(object), options);
+ }
}
- public string Template { get; set; }
- public IV2rayProxy Proxy { get; set; }
- public V2rayRouting Routing { get; set; }
- public V2rayHosts Hosts { get; set; }
- public string ToJson(bool pretty = true)
+ public const string ConfigTemplateFileName = "config.json.template";
+ public const string VmessConfigFileName = "vmess.txt";
+ public const string ProxyConfigFileName = "proxy.txt";
+ public const string HostsConfigFileName = "hosts.txt";
+
+ 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 HostsAnchor = "HOSTS_ANCHOR";
+
+ public Template Template { get; set; } = template;
+ public List<V2rayProxy> Proxies { get; set; } = proxies;
+ public V2rayRouting Routing { get; set; } = router;
+ public V2rayHosts Hosts { get; set; } = hosts is null ? new V2rayHosts([]) : hosts;
+
+ public string ToJsonStringV4(bool pretty = true)
{
var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
+ jsonOptions.Converters.Add(new JsonInterfaceConverter<V2rayV4ConfigJsonObjects.IOutboundSettings>());
+ jsonOptions.Converters.Add(new JsonInterfaceConverter<V2rayV4ConfigJsonObjects.IOutboundStreamSettings>());
var templateValues = new Dictionary<string, string>
{
- [VmessAnchor] = JsonSerializer.Serialize(Proxy.ToOutboundJsonObject(), jsonOptions),
- [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObject(), jsonOptions),
- [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObject(), jsonOptions),
+ [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))),
+ [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions),
+ [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions),
};
- return FileUtility.JsonFormat(FileUtility.TextFromTemplate(Template, templateValues));
+ 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 static V2rayConfig FromFiles(string templatePath, string vmessPath, string routingPath, string hostsPath)
+ public static V2rayConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath)
{
- var template = File.ReadAllText(templatePath);
+ foreach (var path in new List<string>([templatePath, vmessPath, proxyPath]))
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Required config file not found: {path}.");
+ }
+ }
+
+ string templateString, vmessString, routingString;
+ string? hostsString;
+
+ string file = "";
+ try
+ {
+ file = templatePath;
+ templateString = File.ReadAllText(templatePath);
+ file = vmessPath;
+ vmessString = File.ReadAllText(vmessPath);
+ file = proxyPath;
+ routingString = File.ReadAllText(proxyPath);
+ file = proxyPath;
+ hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null;
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"Error reading config file {file}.", e);
+ }
- var vmessDict = FileUtility.ReadDictionaryFile(vmessPath);
- var proxyRoutingList = FileUtility.ReadListFile(routingPath);
- var hostsList = FileUtility.ReadListFile(hostsPath);
+ try
+ {
+ file = templatePath;
+ var template = new Template(templateString);
+ file = vmessPath;
+ var vmess = V2rayVmessProxy.CreateFromConfigString(vmessString, "proxy");
+ file = proxyPath;
+ var routing = V2rayRouting.CreateFromConfigString(routingString, "proxy");
+ file = hostsPath ?? "";
+ var hosts = hostsString is not null ? V2rayHosts.CreateFromConfigString(hostsString) : null;
+ return new V2rayConfig(template, [vmess], routing, hosts);
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"Error parsing config file {file}.", e);
+ }
+ }
- var vmess = V2rayVmessProxy.FromDictionary(vmessDict);
- var routing = V2rayRouting.FromStringList(proxyRoutingList);
- var hosts = V2rayHosts.FromStringList(hostsList);
+ public static V2rayConfig FromDirectory(string directory)
+ {
+ return FromFiles(
+ Path.Join(directory, ConfigTemplateFileName),
+ Path.Join(directory, VmessConfigFileName),
+ Path.Join(directory, ProxyConfigFileName),
+ Path.Join(directory, HostsConfigFileName)
+ );
+ }
- return new V2rayConfig(template, vmess, routing, hosts);
+ public static void FromDirectoryAndWriteToFile(string directory, string outputPath)
+ {
+ var config = FromDirectory(directory);
+ File.WriteAllText(outputPath, config.ToJsonStringV4());
}
}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs
index 201dcf4..ab1614a 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs
@@ -2,69 +2,58 @@ using System.Diagnostics;
namespace Crupest.V2ray;
-public class V2rayController
+public class V2rayController(string executablePath, string configPath, string assetPath)
{
- public static string V2rayExecutableName { get; } = OperatingSystem.IsWindows() ? "v2ray.exe" : "v2ray";
- public const string V2rayExecutableLocationEnvironmentVariableName = "V2RAY_LOCATION_EXE";
- public const string V2rayAssetLocationEnvironmentVariableName = "V2RAY_LOCATION_ASSET";
- public const string V2rayConfigLocationEnvironmentVariableName = "V2RAY_LOCATION_CONFIG";
- public const string V2rayV5ConfdirEnvironmentVariableName = "v2ray.location.confdir";
+ public const string V2rayAssetEnvironmentVariableName = "v2ray.location.asset";
- public V2rayController() : this(V2rayExecutableName, Program.ExeDir, Program.ExeDir)
+ public static string? FindExecutable(string contentDir, string? executableName = null)
{
- var localV2ray = Path.Combine(Program.ExeDir, V2rayExecutableName);
- if (Path.Exists(localV2ray))
+ executableName ??= "v2ray";
+
+ if (OperatingSystem.IsWindows())
{
- V2rayExePath = localV2ray;
+ executableName += ".exe";
}
- }
-
- 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
+ var localV2rayPath = Path.Combine(contentDir, executableName);
+ if (File.Exists(localV2rayPath))
{
- FileName = V2rayExePath,
- };
- startInfo.EnvironmentVariables[V2rayConfigLocationEnvironmentVariableName] = ConfigDirPath;
- startInfo.EnvironmentVariables[V2rayAssetLocationEnvironmentVariableName] = AssetDirPath;
+ return localV2rayPath;
+ }
- process.StartInfo = startInfo;
- process.OutputDataReceived += (_, args) =>
- {
- Console.Out.Write(args.Data);
- };
- process.ErrorDataReceived += (_, args) =>
+ var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator);
+ if (paths is not null)
{
- Console.Error.WriteLine(args.Data);
- };
+ foreach (var p in paths)
+ {
+ var v2rayPath = Path.Combine(p, executableName);
+ if (File.Exists(v2rayPath))
+ {
+ return v2rayPath;
+ }
+ }
+ }
- return process;
+ return null;
}
- private Process V5CreateProcess()
+ public string ExecutablePath { get; } = executablePath;
+ public string ConfigPath { get; } = configPath;
+ public string AssetPath { get; } = assetPath;
+ public Process? CurrentProcess { get; private set; }
+
+ private Process CreateProcess()
{
var process = new Process();
var startInfo = new ProcessStartInfo
{
- FileName = V2rayExePath,
+ FileName = ExecutablePath,
};
startInfo.ArgumentList.Add("run");
- startInfo.EnvironmentVariables[V2rayV5ConfdirEnvironmentVariableName] = ConfigDirPath;
+ startInfo.ArgumentList.Add("-c");
+ startInfo.ArgumentList.Add(ConfigPath);
+ startInfo.EnvironmentVariables[V2rayAssetEnvironmentVariableName] = AssetPath;
process.StartInfo = startInfo;
process.OutputDataReceived += (_, args) =>
@@ -96,13 +85,13 @@ public class V2rayController
if (CurrentProcess is null)
{
- CurrentProcess = V5CreateProcess();
+ CurrentProcess = CreateProcess();
CurrentProcess.EnableRaisingEvents = true;
CurrentProcess.Exited += (_, _) =>
{
if (CurrentProcess.ExitCode != 0)
{
- const string message = "V2ray exits with error.";
+ const string message = "V2ray exited with error.";
Console.Error.WriteLine(message);
throw new Exception(message);
}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs
new file mode 100644
index 0000000..f444b5d
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs
@@ -0,0 +1,95 @@
+namespace Crupest.V2ray;
+
+public enum V2rayHostMatcherKind
+{
+ DomainFull,
+ DomainSuffix,
+ DomainKeyword,
+ DomainRegex,
+ Ip,
+ GeoSite,
+ GeoIp,
+}
+
+public record V2rayHostMatcherItem(V2rayHostMatcherKind Kind, string Matcher, List<string> Values);
+
+public class V2rayHostMatcherConfig(string configString, List<V2rayHostMatcherKind> allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1)
+{
+ static bool IsDomainMatcher(V2rayHostMatcherKind kind) => kind switch
+ {
+ V2rayHostMatcherKind.DomainFull => true,
+ V2rayHostMatcherKind.DomainSuffix => true,
+ V2rayHostMatcherKind.DomainKeyword => true,
+ V2rayHostMatcherKind.DomainRegex => true,
+ _ => false,
+ };
+
+ private static List<V2rayHostMatcherItem> Parse(string configString, List<V2rayHostMatcherKind> allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1)
+ {
+ var items = new ListConfig(configString).Config;
+ var result = new List<V2rayHostMatcherItem>();
+
+ foreach (var item in items)
+ {
+ var lineNumber = item.LineNumber;
+ var line = item.Value;
+ var hasExplicitMatcher = false;
+ var segments = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
+
+ foreach (var matcher in Enum.GetValues<V2rayHostMatcherKind>())
+ {
+ var matcherName = Enum.GetName(matcher) ?? throw new Exception("No such matcher.");
+ hasExplicitMatcher = true;
+ if (segments[0] == matcherName)
+ {
+ if (segments.Count < 2)
+ {
+ throw new FormatException($"Explicit matcher needs a value in line {lineNumber}.");
+ }
+ if (allowedMatchers.Contains(matcher))
+ {
+ if (IsDomainMatcher(matcher) && Uri.CheckHostName(matcherName) != UriHostNameType.Dns)
+ {
+ throw new FormatException($"Invalid domain format in line {lineNumber}.");
+ }
+
+ var components = segments[2..].ToList();
+ if (minComponentCount > 0 && components.Count < minComponentCount)
+ {
+ throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required.");
+ }
+ if (maxComponentCount >= 0 && components.Count > maxComponentCount)
+ {
+ throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed.");
+ }
+ result.Add(new V2rayHostMatcherItem(matcher, segments[1], components));
+ }
+ else
+ {
+ throw new FormatException($"Matcher {matcherName} is not allowed at line {lineNumber}.");
+ }
+ }
+ }
+
+ if (!hasExplicitMatcher)
+ {
+ if (minComponentCount > 0 && segments.Count - 1 < minComponentCount)
+ {
+ throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required.");
+ }
+ if (maxComponentCount >= 0 && segments.Count - 1 > maxComponentCount)
+ {
+ throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed.");
+ }
+ result.Add(new V2rayHostMatcherItem(V2rayHostMatcherKind.DomainSuffix, segments[0], segments.Count == 1 ? [] : segments[1..]));
+ }
+ }
+ return result;
+ }
+
+ public string ConfigString { get; } = configString;
+ public List<V2rayHostMatcherKind> AllowedMatchers { get; } = allowedMatchers;
+ public int MinComponentCount { get; } = minComponentCount;
+ public int MaxComponentCount { get; } = maxComponentCount;
+ public List<V2rayHostMatcherItem> Items { get; } = Parse(configString, allowedMatchers, minComponentCount, maxComponentCount);
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs
deleted file mode 100644
index 31feaaf..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace Crupest.V2ray;
-
-public record V2rayHostRule(string Origin, List<string> Resolved)
-{
- public static V2rayHostRule Parse(string str)
- {
- var segments = str.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
- if (segments.Length == 1)
- {
- throw new Exception("Host rule only contains 1 segment.");
- }
-
- var resolved = new List<string>();
- resolved.AddRange(segments[1..]);
-
- return new V2rayHostRule(segments[0], resolved);
- }
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs
index a1c8d61..1d1c6d8 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs
@@ -1,26 +1,42 @@
namespace Crupest.V2ray;
-public record V2rayHosts(List<V2rayHostRule> Rules)
+public record V2rayHostRule(V2rayHostMatcherKind MatcherKind, string MatcherString, List<string> ResolveResult)
{
- public V2rayHosts() : this(new List<V2rayHostRule>()) { }
-
- public Dictionary<string, List<string>> ToJsonObject()
+ public string AddressString()
{
- var result = new Dictionary<string, List<string>>();
- foreach (var rule in Rules)
+ return MatcherKind switch
{
- result.Add(rule.Origin, rule.Resolved);
- }
- return result;
+ V2rayHostMatcherKind.DomainFull => MatcherString,
+ V2rayHostMatcherKind.DomainSuffix => $"domain:{MatcherString}",
+ V2rayHostMatcherKind.DomainKeyword => $"keyword:{MatcherString}",
+ V2rayHostMatcherKind.DomainRegex => $"regexp:{MatcherString}",
+ _ => throw new ArgumentOutOfRangeException($"Matcher {MatcherKind} is not allowed in host rule."),
+ };
}
- public static V2rayHosts FromStringList(List<string> list)
+ public object ResolveResultToJsonObject()
{
- var hosts = new V2rayHosts();
- foreach (var str in list)
- {
- hosts.Rules.Add(V2rayHostRule.Parse(str));
- }
- return hosts;
+ return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult;
+ }
+}
+
+public class V2rayHosts(List<V2rayHostRule> rules) : IV2rayV4ConfigObject
+{
+ public List<V2rayHostRule> Rules { get; } = rules;
+
+ public Dictionary<string, object> ToJsonObjectV4() =>
+ Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject());
+
+ object IV2rayV4ConfigObject.ToJsonObjectV4()
+ {
+ return ToJsonObjectV4();
+ }
+
+ public static V2rayHosts CreateFromConfigString(string configString)
+ {
+ var matcherConfig = new V2rayHostMatcherConfig(configString,
+ [V2rayHostMatcherKind.DomainFull, V2rayHostMatcherKind.DomainKeyword, V2rayHostMatcherKind.DomainRegex, V2rayHostMatcherKind.DomainSuffix], minComponentCount: 1);
+
+ return new V2rayHosts(matcherConfig.Items.Select(i => new V2rayHostRule(i.Kind, i.Matcher, [.. i.Values])).ToList());
}
}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs
deleted file mode 100644
index c641b4b..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-namespace Crupest.V2ray;
-
-public class V2rayHttpProxy : IV2rayProxy
-{
- public record HttpOutboundJsonObject(string Protocol, SettingsJsonObject Settings, string Tag)
- {
- public static HttpOutboundJsonObject Create(string address, int port, string tag)
- {
- return new HttpOutboundJsonObject("http", new SettingsJsonObject(
- new List<ServerJsonObject> { new ServerJsonObject(address, port) }
- ), tag);
- }
- }
-
- public record ServerJsonObject(string Address, int Port);
- public record SettingsJsonObject(List<ServerJsonObject> Servers);
-
- public string Host { get; set; }
- public int Port { get; set; }
-
- public V2rayHttpProxy(string host, int port)
- {
- Host = host;
- Port = port;
- }
-
- public HttpOutboundJsonObject ToOutboundJsonObject(string tag = "proxy")
- {
- return HttpOutboundJsonObject.Create(Host, Port, tag);
- }
-
- object IV2rayProxy.ToOutboundJsonObject()
- {
- return ToOutboundJsonObject();
- }
-
- public static V2rayHttpProxy FromDictionary(Dictionary<string, string> dict)
- {
- return new V2rayHttpProxy(dict["host"], int.Parse(dict["port"]));
- }
-}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs
new file mode 100644
index 0000000..bcb2b51
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs
@@ -0,0 +1,58 @@
+namespace Crupest.V2ray;
+
+public abstract class V2rayProxy(string tag) : IV2rayV4ConfigObject
+{
+ public string Tag { get; set; } = tag;
+
+ public abstract V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4();
+
+ object IV2rayV4ConfigObject.ToJsonObjectV4()
+ {
+ return ToJsonObjectV4();
+ }
+}
+
+public class V2rayHttpProxy(string host, int port, string tag) : V2rayProxy(tag)
+{
+ public string Host { get; set; } = host;
+ public int Port { get; set; } = port;
+
+ public override V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4()
+ {
+ return new V2rayV4ConfigJsonObjects.Outbound(Tag, "http",
+ new V2rayV4ConfigJsonObjects.HttpOutboundSettings([new V2rayV4ConfigJsonObjects.HttpOutboundServer(Host, Port, [])]),
+ null
+ );
+ }
+}
+
+
+public class V2rayVmessProxy(string host, int port, string userId, string path, string tag) : V2rayProxy(tag)
+{
+ public string Host { get; set; } = host;
+ public int Port { get; set; } = port;
+ public string Path { get; set; } = path;
+ public string UserId { get; set; } = userId;
+
+ public override V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4()
+ {
+ return new V2rayV4ConfigJsonObjects.Outbound(Tag, "vmess",
+ new V2rayV4ConfigJsonObjects.VmessOutboundSettings(
+ [new V2rayV4ConfigJsonObjects.VnextServer(Host, Port, [new V2rayV4ConfigJsonObjects.VnextServerUser(UserId, 0, "auto", 0)])]),
+ new V2rayV4ConfigJsonObjects.WsStreamSettings("ws", "tls", new V2rayV4ConfigJsonObjects.WsSettings(Path, new() { ["Host"] = Host }))
+ );
+ }
+
+ public static V2rayVmessProxy CreateFromConfigString(string configString, string tag)
+ {
+ var config = new DictionaryConfig(configString, ["host", "port", "userid", "path"]);
+ var portString = config.GetItemCaseInsensitive("port").Value;
+ if (!int.TryParse(portString, out var port) || port <= 0)
+ {
+ throw new FormatException($"Invalid port number: {portString}: not an integer or is a invalid number.");
+ }
+ return new V2rayVmessProxy(config.GetItemCaseInsensitive("host").Value, port,
+ config.GetItemCaseInsensitive("userid").Value, config.GetItemCaseInsensitive("path").Value, tag
+ );
+ }
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs
index d5ad0da..528b49e 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs
@@ -1,65 +1,72 @@
namespace Crupest.V2ray;
-public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand")
+public record V2rayRoutingRule(V2rayHostMatcherKind MatcherKind, string MatcherString, string OutboundTag) : IV2rayV4ConfigObject
{
- public record DomainRuleJsonObject(List<string> Domains, string OutboundTag, string Type = "field");
-
- public record IpRuleJsonObject(List<string> Ip, string OutboundTag, string Type = "field");
-
- public record V5DomainRuleJsonObject(List<V2rayRoutingRuleMatcher.V5DomainObject> Domain, string OutboundTag);
+ public string ComposedMatcherString => MatcherKind switch
+ {
+ V2rayHostMatcherKind.DomainFull => $"full:{MatcherString}",
+ V2rayHostMatcherKind.DomainSuffix => $"domain:{MatcherString}",
+ V2rayHostMatcherKind.DomainKeyword => MatcherString,
+ V2rayHostMatcherKind.DomainRegex => $"regexp:{MatcherString}",
+ V2rayHostMatcherKind.Ip => MatcherString,
+ V2rayHostMatcherKind.GeoSite => $"geosite:{MatcherString}",
+ V2rayHostMatcherKind.GeoIp => $"geoip:{MatcherString}",
+ _ => throw new ArgumentException("Invalid matcher kind.")
+ };
+
+ 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)
+ => rules.GroupBy(r => r.MatcherKind).Select(g => (g.Key, g.ToList())).ToDictionary();
+
+ public static V2rayV4ConfigJsonObjects.RoutingRule ListToJsonObject(List<V2rayRoutingRule> rules)
+ {
+ if (rules.Count == 0)
+ {
+ throw new ArgumentException("Rule list is empty.");
+ }
- public record V5GeoDomainRuleJsonObject(List<V2rayRoutingRuleMatcher.V5GeoDomainObject> GeoDomain, string OutboundTag);
+ var matcherKind = rules[0].MatcherKind;
+ var outboundTag = rules[0].OutboundTag;
- public record V5GeoIpRuleJsonObject(List<V2rayRoutingRuleMatcher.V5GeoIpObject> Geoip, string OutboundTag);
+ if (rules.Any(r => r.OutboundTag != outboundTag) || rules.Any(r => r.MatcherKind != matcherKind))
+ {
+ throw new ArgumentException("Rules must have the same matcher kind and outbound tag.");
+ }
- public record RoutingJsonObject(string DomainStrategy, List<object> Rules);
+ List<string> composedMatcherStringList = rules.Select(r => r.ComposedMatcherString).ToList();
- public record V5RouterJsonObject(string DomainStrategy, List<object> Rule);
+ return new V2rayV4ConfigJsonObjects.RoutingRule(OutboundTag: outboundTag,
+ Ip: (matcherKind is V2rayHostMatcherKind.Ip or V2rayHostMatcherKind.GeoIp) ? composedMatcherStringList : null,
+ Domains: (matcherKind is V2rayHostMatcherKind.DomainFull or V2rayHostMatcherKind.DomainSuffix or V2rayHostMatcherKind.DomainKeyword or V2rayHostMatcherKind.DomainRegex or V2rayHostMatcherKind.GeoSite) ? composedMatcherStringList : null
+ );
+ }
- public V2rayRouting() : this(new List<V2rayRoutingRule>())
- {
+ public V2rayV4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]);
- }
+ object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
+}
- public RoutingJsonObject ToJsonObject()
+public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand") : IV2rayV4ConfigObject
+{
+ public V2rayV4ConfigJsonObjects.Routing ToJsonObjectV4()
{
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."),
- }
- );
- }
- }
+ var rules = V2rayRoutingRule.GroupByOutboundTag(Rules).ToList().SelectMany((groupByTag) =>
+ V2rayRoutingRule.GroupByMatcherByKind(groupByTag.Value).ToList().Select((groupByMatcher) =>
+ V2rayRoutingRule.ListToJsonObject(groupByMatcher.Value))
+ ).ToList();
- return new RoutingJsonObject(DomainStrategy, ruleJsonObjects);
+ return new V2rayV4ConfigJsonObjects.Routing(rules);
}
- public V5RouterJsonObject V5ToJsonObject()
- {
- throw new NotImplementedException();
- }
+ object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4();
- public static V2rayRouting FromStringList(List<string> list, string outboundTag = "proxy")
+ public static V2rayRouting CreateFromConfigString(string configString, string outboundTag)
{
- 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;
+ 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());
}
}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs
deleted file mode 100644
index 1928de0..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-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;
- }
-
- public static Dictionary<V2rayRoutingRuleMatcher.V5MatchByKind, List<V2rayRoutingRule>> V5GroupByMatchByKind(List<V2rayRoutingRule> rules)
- {
- var result = new Dictionary<V2rayRoutingRuleMatcher.V5MatchByKind, List<V2rayRoutingRule>>();
- foreach (var group in rules.GroupBy(r => r.Matcher.V5MatchBy))
- {
- 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
deleted file mode 100644
index 7c853c4..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-namespace Crupest.V2ray;
-
-public record V2rayRoutingRuleMatcher(V2rayRoutingRuleMatcher.MatchKind Kind, string Value)
-{
- public enum MatchByKind
- {
- Domain,
- Ip
- }
-
- public enum V5MatchByKind
- {
- Domain,
- // Ip,
- GeoIp,
- GeoSite,
- }
-
- public enum MatchKind
- {
- GeoIp,
- GeoSite,
- DomainPlain,
- DomainSuffix,
- DomainRegex,
- DomainFull,
- }
-
- public MatchByKind MatchBy
- {
- get
- {
- return Kind switch
- {
- MatchKind.GeoIp => MatchByKind.Ip,
- _ => MatchByKind.Domain
- };
- }
- }
-
- public V5MatchByKind V5MatchBy
- {
- get
- {
- return Kind switch
- {
- MatchKind.GeoIp => V5MatchByKind.GeoIp,
- MatchKind.GeoSite => V5MatchByKind.GeoSite,
- _ => V5MatchByKind.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."),
- };
- }
-
- public enum V5DomainObjectType
- {
- Plain,
- Regex,
- RootDomain,
- Full,
- }
-
- public record V5DomainObject(V5DomainObjectType Type, string Value);
-
- public V5DomainObject ToDomainObject()
- {
- return new V5DomainObject(Kind switch
- {
- MatchKind.DomainFull => V5DomainObjectType.Full,
- MatchKind.DomainPlain => V5DomainObjectType.Plain,
- MatchKind.DomainRegex => V5DomainObjectType.Regex,
- MatchKind.DomainSuffix => V5DomainObjectType.RootDomain,
- _ => throw new Exception("Not a domain matcher."),
- }, Value);
- }
-
- public record V5GeoDomainObject(string Code);
-
- public V5GeoDomainObject ToGeoDomainObject()
- {
- if (Kind != MatchKind.GeoSite)
- {
- throw new Exception("Not a geo-domain matcher.");
- }
-
- return new V5GeoDomainObject(Value);
- }
-
- public record V5GeoIpObject(string Code);
-
- public V5GeoIpObject ToGeoIpObject()
- {
- if (Kind != MatchKind.GeoIp)
- {
- throw new Exception("Not a geo-ip matcher.");
- }
-
- return new V5GeoIpObject(Value);
- }
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs
deleted file mode 100644
index 88ea5ba..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Crupest.V2ray;
-
-public class V2rayStaticHostDomainResolveResult : IV2rayStaticHostResolveResult
-{
- public V2rayStaticHostDomainResolveResult(string domain)
- {
- Domain = domain;
- }
-
- public string Domain { get; }
-
- public IDictionary<string, object> GetJsonProperties()
- {
- return new Dictionary<string, object>
- {
-
- ["proxiedDomain"] = Domain
- };
- }
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs
deleted file mode 100644
index 2829152..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Crupest.V2ray;
-
-public class V2rayStaticHostIpResolveResult : IV2rayStaticHostResolveResult
-{
- public V2rayStaticHostIpResolveResult(IEnumerable<string> ips)
- {
- Ips = ips.ToList();
- }
-
- public IReadOnlyList<string> Ips { get; }
-
- public IDictionary<string, object> GetJsonProperties()
- {
- return new Dictionary<string, object>
- {
- ["ip"] = Ips
- };
- }
-}
-
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs
new file mode 100644
index 0000000..672af71
--- /dev/null
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs
@@ -0,0 +1,25 @@
+namespace Crupest.V2ray;
+
+public static class V2rayV4ConfigJsonObjects
+{
+ public interface IObject;
+ public interface IOutboundSettings : IObject;
+ public interface IOutboundStreamSettings : IObject;
+
+ public record WsSettings(string Path, Dictionary<string, string> Headers) : IObject;
+ public record WsStreamSettings(string Network, string Security, WsSettings WsSettings) : IOutboundStreamSettings;
+ public record VnextServerUser(string Id, int AlterId, string Security, int Level) : IObject;
+ public record VnextServer(string Address, int Port, List<VnextServerUser> Users) : IObject;
+ public record VmessOutboundSettings(List<VnextServer> Vnext) : IOutboundSettings;
+ public record HttpOutboundUser(string User, string Pass) : IObject;
+ public record HttpOutboundServer(string Address, int Port, List<HttpOutboundUser> Users) : IObject;
+ public record HttpOutboundSettings(List<HttpOutboundServer> Servers) : IOutboundSettings;
+ public record Outbound(string Tag, string Protocol, IOutboundSettings Settings,
+ IOutboundStreamSettings? StreamSettings) : IObject;
+
+ public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List<string>? Domains = null, List<string>? Ip = null,
+ string? Port = null, string? SourcePort = null, string? Network = null, List<string>? Source = null,
+ List<string>? User = null, List<string>? InboundTag = null, List<string>? Protocol = null, string? Attrs = null,
+ string? OutboundTag = null, string? BalancerTag = null) : IObject;
+ public record Routing(List<RoutingRule> Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject;
+}
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs
index f4001c1..56d64ca 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigObjects.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs
@@ -1,6 +1,6 @@
namespace Crupest.V2ray;
-public static class V2rayV5ConfigObjects
+public static class V2rayV5ConfigJsonObjects
{
public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings)
{
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs
index 4a89134..cdead3c 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs
+++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs
@@ -2,7 +2,50 @@ using System.Net;
namespace Crupest.V2ray;
-public class V2rayV5StaticHostRule
+public interface IV2rayStaticHostResolveResult
+{
+ IDictionary<string, object> GetJsonProperties();
+}
+
+public class V2rayStaticHostDomainResolveResult : IV2rayStaticHostResolveResult
+{
+ public V2rayStaticHostDomainResolveResult(string domain)
+ {
+ Domain = domain;
+ }
+
+ public string Domain { get; }
+
+ public IDictionary<string, object> GetJsonProperties()
+ {
+ return new Dictionary<string, object>
+ {
+
+ ["proxiedDomain"] = Domain
+ };
+ }
+}
+
+public class V2rayStaticHostIpResolveResult : IV2rayStaticHostResolveResult
+{
+ public V2rayStaticHostIpResolveResult(IEnumerable<string> ips)
+ {
+ Ips = ips.ToList();
+ }
+
+ public IReadOnlyList<string> Ips { get; }
+
+ public IDictionary<string, object> GetJsonProperties()
+ {
+ return new Dictionary<string, object>
+ {
+ ["ip"] = Ips
+ };
+ }
+}
+
+
+public class V2rayV5StaticHostRule(V2rayV5StaticHostRule.MatcherKind matcher, string domain, IV2rayStaticHostResolveResult resolveResult)
{
public enum MatcherKind
{
@@ -12,16 +55,9 @@ public class V2rayV5StaticHostRule
Regex
}
- public V2rayV5StaticHostRule(MatcherKind matcher, string domain, IV2rayStaticHostResolveResult resolveResult)
- {
- Matcher = matcher;
- Domain = domain;
- ResolveResult = resolveResult;
- }
-
- public MatcherKind Matcher { get; }
- public string Domain { get; }
- public IV2rayStaticHostResolveResult ResolveResult { get; }
+ public MatcherKind Matcher { get; } = matcher;
+ public string Domain { get; } = domain;
+ public IV2rayStaticHostResolveResult ResolveResult { get; } = resolveResult;
public Dictionary<string, object> ToJsonObject()
{
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs
deleted file mode 100644
index 495961c..0000000
--- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-namespace Crupest.V2ray;
-
-public class V2rayVmessProxy : IV2rayProxy
-{
- 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 WsSettingsJsonObject(string Path, Dictionary<string, string> Headers);
-
- 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 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 V2rayV5ConfigObjects.OutboundObject ToOutboundJsonObjectV5(string tag = "proxy")
- {
- return V2rayV5ConfigObjects.OutboundObject.VmessViaWs(tag, Host, Port, UserId, Path);
- }
-
- object IV2rayProxy.ToOutboundJsonObject()
- {
- return ToOutboundJsonObject();
- }
-
- 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
index 53aee76..424e996 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template
+++ b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template
@@ -33,7 +33,7 @@
"settings": {},
"tag": "blocked"
},
- ${VMESS_PROXY_ANCHOR}
+ ${PROXY_ANCHOR}
],
"routing": ${ROUTING_ANCHOR},
"dns": {
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template
index 49d97c2..01ccf7a 100644
--- a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template
+++ b/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template
@@ -15,7 +15,7 @@
}, {
"address": "localhost"
}],
- "staticHosts": ${STATIC_HOSTS_ANCHOR}
+ "staticHosts": ${HOSTS_ANCHOR}
},
"inbounds": [{
{
@@ -48,7 +48,7 @@
"settings": {},
"tag": "blocked"
},
- ${PROXY_OUTBOUND_ANCHOR}
+ ${PROXY_ANCHOR}
],
"router": ${ROUTER_ANCHOR}
}