diff --git a/v2rayN/v2rayN/App.xaml b/v2rayN/v2rayN/App.xaml
index de82e83f..42e6dffc 100644
--- a/v2rayN/v2rayN/App.xaml
+++ b/v2rayN/v2rayN/App.xaml
@@ -1,9 +1,9 @@
@@ -20,6 +20,7 @@
13
14
11
+ 11
+
+
+
+
\ No newline at end of file
diff --git a/v2rayN/v2rayN/Common/HttpClientHelper.cs b/v2rayN/v2rayN/Common/HttpClientHelper.cs
index 1c338b61..60f9ad2b 100644
--- a/v2rayN/v2rayN/Common/HttpClientHelper.cs
+++ b/v2rayN/v2rayN/Common/HttpClientHelper.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
@@ -21,6 +22,22 @@ namespace v2rayN
private HttpClientHelper(HttpClient httpClient) => this.httpClient = httpClient;
+ public async Task TryGetAsync(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ return null;
+
+ try
+ {
+ HttpResponseMessage response = await httpClient.GetAsync(url);
+ return await response.Content.ReadAsStringAsync();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
public async Task GetAsync(string url)
{
if (Utils.IsNullOrEmpty(url)) return null;
@@ -41,6 +58,21 @@ namespace v2rayN
var result = await httpClient.PutAsync(url, content);
}
+ public async Task PatchAsync(string url, Dictionary headers)
+ {
+ var myContent = JsonUtils.Serialize(headers);
+ var buffer = System.Text.Encoding.UTF8.GetBytes(myContent);
+ var byteContent = new ByteArrayContent(buffer);
+ byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+
+ await httpClient.PatchAsync(url, byteContent);
+ }
+
+ public async Task DeleteAsync(string url)
+ {
+ await httpClient.DeleteAsync(url);
+ }
+
public static async Task DownloadFileAsync(HttpClient client, string url, string fileName, IProgress? progress, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(url);
diff --git a/v2rayN/v2rayN/Common/YamlUtils.cs b/v2rayN/v2rayN/Common/YamlUtils.cs
new file mode 100644
index 00000000..57af2119
--- /dev/null
+++ b/v2rayN/v2rayN/Common/YamlUtils.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace v2rayN.Common
+{
+ internal class YamlUtils
+ {
+ #region YAML
+
+ ///
+ /// 反序列化成对象
+ ///
+ ///
+ ///
+ ///
+ public static T FromYaml(string str)
+ {
+ var deserializer = new DeserializerBuilder()
+ .WithNamingConvention(PascalCaseNamingConvention.Instance)
+ .Build();
+ try
+ {
+ T obj = deserializer.Deserialize(str);
+ return obj;
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("FromYaml", ex);
+ return deserializer.Deserialize("");
+ }
+ }
+
+ ///
+ /// 序列化
+ ///
+ ///
+ ///
+ public static string ToYaml(Object obj)
+ {
+ var serializer = new SerializerBuilder()
+ .WithNamingConvention(HyphenatedNamingConvention.Instance)
+ .Build();
+
+ string result = string.Empty;
+ try
+ {
+ result = serializer.Serialize(obj);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ return result;
+ }
+
+ #endregion YAML
+ }
+}
diff --git a/v2rayN/v2rayN/Enums/ERuleMode.cs b/v2rayN/v2rayN/Enums/ERuleMode.cs
new file mode 100644
index 00000000..59a01430
--- /dev/null
+++ b/v2rayN/v2rayN/Enums/ERuleMode.cs
@@ -0,0 +1,10 @@
+namespace v2rayN.Enums
+{
+ public enum ERuleMode
+ {
+ Rule = 0,
+ Global = 1,
+ Direct = 2,
+ Unchanged = 3
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN/Global.cs b/v2rayN/v2rayN/Global.cs
index 067b53ea..17d39a5a 100644
--- a/v2rayN/v2rayN/Global.cs
+++ b/v2rayN/v2rayN/Global.cs
@@ -183,6 +183,10 @@ namespace v2rayN
public static readonly List SingboxMuxs = new() { "h2mux", "smux", "yamux", "" };
public static readonly List TuicCongestionControls = new() { "cubic", "new_reno", "bbr" };
+ public static readonly List allowSelectType = new List { "selector", "urltest", "loadbalance", "fallback" };
+ public static readonly List notAllowTestType = new List { "selector", "urltest", "direct", "reject", "compatible", "pass", "loadbalance", "fallback" };
+ public static readonly List proxyVehicleType = new List { "file", "http" };
+
#endregion const
}
}
\ No newline at end of file
diff --git a/v2rayN/v2rayN/Handler/ClashApiHandler.cs b/v2rayN/v2rayN/Handler/ClashApiHandler.cs
new file mode 100644
index 00000000..45efb8e6
--- /dev/null
+++ b/v2rayN/v2rayN/Handler/ClashApiHandler.cs
@@ -0,0 +1,219 @@
+using v2rayN.Models;
+using static v2rayN.Models.ClashProxies;
+
+namespace v2rayN.Handler
+{
+ public sealed class ClashApiHandler
+ {
+ private static readonly Lazy instance = new(() => new());
+ public static ClashApiHandler Instance => instance.Value;
+
+ private Dictionary _proxies;
+ public Dictionary ProfileContent { get; set; }
+ public bool ShowInTaskbar { get; set; } = true;
+
+ public void SetProxies(Dictionary proxies)
+ {
+ _proxies = proxies;
+ }
+
+ public Dictionary GetProxies()
+ {
+ return _proxies;
+ }
+
+ public void GetClashProxies(Config config, Action update)
+ {
+ Task.Run(() => GetClashProxiesAsync(config, update));
+ }
+
+ private async Task GetClashProxiesAsync(Config config, Action update)
+ {
+ for (var i = 0; i < 5; i++)
+ {
+ var url = $"{GetApiUrl()}/proxies";
+ var result = await HttpClientHelper.Instance.TryGetAsync(url);
+ var clashProxies = JsonUtils.Deserialize(result);
+
+ var url2 = $"{GetApiUrl()}/providers/proxies";
+ var result2 = await HttpClientHelper.Instance.TryGetAsync(url2);
+ var clashProviders = JsonUtils.Deserialize(result2);
+
+ if (clashProxies != null || clashProviders != null)
+ {
+ update(clashProxies, clashProviders);
+ return;
+ }
+ Thread.Sleep(5000);
+ }
+ update(null, null);
+ }
+
+ public void ClashProxiesDelayTest(bool blAll, List lstProxy, Action update)
+ {
+ Task.Run(() =>
+ {
+ if (blAll)
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ if (GetProxies() != null)
+ {
+ break;
+ }
+ Thread.Sleep(5000);
+ }
+ var proxies = GetProxies();
+ if (proxies == null)
+ {
+ return;
+ }
+ lstProxy = new List();
+ foreach (KeyValuePair kv in proxies)
+ {
+ if (Global.notAllowTestType.Contains(kv.Value.type.ToLower()))
+ {
+ continue;
+ }
+ lstProxy.Add(new ClashProxyModel()
+ {
+ name = kv.Value.name,
+ type = kv.Value.type.ToLower(),
+ });
+ }
+ }
+
+ if (lstProxy == null)
+ {
+ return;
+ }
+ var urlBase = $"{GetApiUrl()}/proxies";
+ urlBase += @"/{0}/delay?timeout=10000&url=" + LazyConfig.Instance.GetConfig().speedTestItem.speedPingTestUrl;
+
+ List tasks = new List();
+ foreach (var it in lstProxy)
+ {
+ if (Global.notAllowTestType.Contains(it.type.ToLower()))
+ {
+ continue;
+ }
+ var name = it.name;
+ var url = string.Format(urlBase, name);
+ tasks.Add(Task.Run(async () =>
+ {
+ var result = await HttpClientHelper.Instance.TryGetAsync(url);
+ update(it, result);
+ }));
+ }
+ Task.WaitAll(tasks.ToArray());
+
+ Thread.Sleep(1000);
+ update(null, "");
+ });
+ }
+
+ public List? GetClashProxyGroups()
+ {
+ try
+ {
+ var fileContent = ProfileContent;
+ if (fileContent is null || fileContent?.ContainsKey("proxy-groups") == false)
+ {
+ return null;
+ }
+ return JsonUtils.Deserialize>(JsonUtils.Serialize(fileContent["proxy-groups"]));
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("GetClashProxyGroups", ex);
+ return null;
+ }
+ }
+
+ public async void ClashSetActiveProxy(string name, string nameNode)
+ {
+ try
+ {
+ var url = $"{GetApiUrl()}/proxies/{name}";
+ Dictionary headers = new Dictionary();
+ headers.Add("name", nameNode);
+ await HttpClientHelper.Instance.PutAsync(url, headers);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ }
+
+ public void ClashConfigUpdate(Dictionary headers)
+ {
+ Task.Run(async () =>
+ {
+ var proxies = GetProxies();
+ if (proxies == null)
+ {
+ return;
+ }
+
+ var urlBase = $"{GetApiUrl()}/configs";
+
+ await HttpClientHelper.Instance.PatchAsync(urlBase, headers);
+ });
+ }
+
+ public async void ClashConfigReload(string filePath)
+ {
+ ClashConnectionClose("");
+ try
+ {
+ var url = $"{GetApiUrl()}/configs?force=true";
+ Dictionary headers = new Dictionary();
+ headers.Add("path", filePath);
+ await HttpClientHelper.Instance.PutAsync(url, headers);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ }
+
+ public void GetClashConnections(Config config, Action update)
+ {
+ Task.Run(() => GetClashConnectionsAsync(config, update));
+ }
+
+ private async Task GetClashConnectionsAsync(Config config, Action update)
+ {
+ try
+ {
+ var url = $"{GetApiUrl()}/connections";
+ var result = await HttpClientHelper.Instance.TryGetAsync(url);
+ var clashConnections = JsonUtils.Deserialize(result);
+
+ update(clashConnections);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ }
+
+ public async void ClashConnectionClose(string id)
+ {
+ try
+ {
+ var url = $"{GetApiUrl()}/connections/{id}";
+ await HttpClientHelper.Instance.DeleteAsync(url);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ }
+
+ private string GetApiUrl()
+ {
+ return $"{Global.HttpProtocol}{Global.Loopback}:{LazyConfig.Instance.StatePort2}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN/Handler/ConfigHandler.cs b/v2rayN/v2rayN/Handler/ConfigHandler.cs
index 526c203b..0f789d96 100644
--- a/v2rayN/v2rayN/Handler/ConfigHandler.cs
+++ b/v2rayN/v2rayN/Handler/ConfigHandler.cs
@@ -194,6 +194,7 @@ namespace v2rayN.Handler
down_mbps = 100
};
}
+ config.clashUIItem ??= new();
LazyConfig.Instance.SetConfig(config);
return 0;
diff --git a/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs
new file mode 100644
index 00000000..900c3fd5
--- /dev/null
+++ b/v2rayN/v2rayN/Handler/CoreConfig/CoreConfigClash.cs
@@ -0,0 +1,259 @@
+using System.IO;
+using v2rayN.Common;
+using v2rayN.Enums;
+using v2rayN.Models;
+using v2rayN.Resx;
+
+namespace v2rayN.Handler.CoreConfig
+{
+ ///
+ /// Core configuration file processing class
+ ///
+ internal class CoreConfigClash
+ {
+ private Config _config;
+
+ public CoreConfigClash(Config config)
+ {
+ _config = config;
+ }
+
+ ///
+ /// 生成配置文件
+ ///
+ ///
+ ///
+ ///
+ ///
+ public int GenerateClientConfig(ProfileItem node, string? fileName, out string msg)
+ {
+ if (node == null || fileName is null)
+ {
+ msg = ResUI.CheckServerSettings;
+ return -1;
+ }
+
+ msg = ResUI.InitialConfiguration;
+
+ try
+ {
+ if (node == null)
+ {
+ msg = ResUI.CheckServerSettings;
+ return -1;
+ }
+
+ if (File.Exists(fileName))
+ {
+ File.Delete(fileName);
+ }
+
+ string addressFileName = node.address;
+ if (string.IsNullOrEmpty(addressFileName))
+ {
+ msg = ResUI.FailedGetDefaultConfiguration;
+ return -1;
+ }
+ if (!File.Exists(addressFileName))
+ {
+ addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName);
+ }
+ if (!File.Exists(addressFileName))
+ {
+ msg = ResUI.FailedReadConfiguration + "1";
+ return -1;
+ }
+
+ string tagYamlStr1 = "!";
+ string tagYamlStr2 = "__strn__";
+ string tagYamlStr3 = "!!str";
+ var txtFile = File.ReadAllText(addressFileName);
+ txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
+
+ var fileContent = YamlUtils.FromYaml>(txtFile);
+ if (fileContent == null)
+ {
+ msg = ResUI.FailedConversionConfiguration;
+ return -1;
+ }
+
+ //port
+ fileContent["port"] = LazyConfig.Instance.GetLocalPort(EInboundProtocol.http);
+ //socks-port
+ fileContent["socks-port"] = LazyConfig.Instance.GetLocalPort(EInboundProtocol.socks);
+ //log-level
+ fileContent["log-level"] = _config.coreBasicItem.loglevel;
+ //external-controller
+ fileContent["external-controller"] = $"{Global.Loopback}:{LazyConfig.Instance.StatePort2}";
+ //allow-lan
+ if (_config.inbound[0].allowLANConn)
+ {
+ fileContent["allow-lan"] = "true";
+ fileContent["bind-address"] = "*";
+ }
+ else
+ {
+ fileContent["allow-lan"] = "false";
+ }
+
+ //ipv6
+ //fileContent["ipv6"] = _config.EnableIpv6;
+
+ //mode
+ if (!fileContent.ContainsKey("mode"))
+ {
+ fileContent["mode"] = ERuleMode.Rule.ToString().ToLower();
+ }
+ else
+ {
+ if (_config.clashUIItem.ruleMode != ERuleMode.Unchanged)
+ {
+ fileContent["mode"] = _config.clashUIItem.ruleMode.ToString().ToLower();
+ }
+ }
+
+ ////enable tun mode
+ //if (config.EnableTun)
+ //{
+ // string tun = Utils.GetEmbedText(Global.SampleTun);
+ // if (!string.IsNullOrEmpty(tun))
+ // {
+ // var tunContent = Utils.FromYaml>(tun);
+ // if (tunContent != null)
+ // fileContent["tun"] = tunContent["tun"];
+ // }
+ //}
+
+ //Mixin
+ //try
+ //{
+ // MixinContent(fileContent, config, node);
+ //}
+ //catch (Exception ex)
+ //{
+ // Logging.SaveLog("GenerateClientConfigClash-Mixin", ex);
+ //}
+
+ var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3);
+ File.WriteAllText(fileName, txtFileNew);
+ //check again
+ if (!File.Exists(fileName))
+ {
+ msg = ResUI.FailedReadConfiguration + "2";
+ return -1;
+ }
+
+ ClashApiHandler.Instance.ProfileContent = fileContent;
+
+ msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}");
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("GenerateClientConfigClash", ex);
+ msg = ResUI.FailedGenDefaultConfiguration;
+ return -1;
+ }
+ return 0;
+ }
+
+ //private static void MixinContent(Dictionary fileContent, Config config, ProfileItem node)
+ //{
+ // if (!config.EnableMixinContent)
+ // {
+ // return;
+ // }
+
+ // var path = Utils.GetConfigPath(Global.mixinConfigFileName);
+ // if (!File.Exists(path))
+ // {
+ // return;
+ // }
+
+ // var txtFile = File.ReadAllText(Utils.GetConfigPath(Global.mixinConfigFileName));
+ // //txtFile = txtFile.Replace("!", "");
+
+ // var mixinContent = YamlUtils.FromYaml>(txtFile);
+ // if (mixinContent == null)
+ // {
+ // return;
+ // }
+ // foreach (var item in mixinContent)
+ // {
+ // if (!config.EnableTun && item.Key == "tun")
+ // {
+ // continue;
+ // }
+
+ // if (item.Key.StartsWith("prepend-")
+ // || item.Key.StartsWith("append-")
+ // || item.Key.StartsWith("removed-"))
+ // {
+ // ModifyContentMerge(fileContent, item.Key, item.Value);
+ // }
+ // else
+ // {
+ // fileContent[item.Key] = item.Value;
+ // }
+ // }
+ // return;
+ //}
+
+ private static void ModifyContentMerge(Dictionary fileContent, string key, object value)
+ {
+ bool blPrepend = false;
+ bool blRemoved = false;
+ if (key.StartsWith("prepend-"))
+ {
+ blPrepend = true;
+ key = key.Replace("prepend-", "");
+ }
+ else if (key.StartsWith("append-"))
+ {
+ blPrepend = false;
+ key = key.Replace("append-", "");
+ }
+ else if (key.StartsWith("removed-"))
+ {
+ blRemoved = true;
+ key = key.Replace("removed-", "");
+ }
+ else
+ {
+ return;
+ }
+
+ if (!blRemoved && !fileContent.ContainsKey(key))
+ {
+ fileContent.Add(key, value);
+ return;
+ }
+ var lstOri = (List