Clash代理提取合并

缘由

Surge是一款功能很强大的代理软件,可惜只有Apple平台的产品。对于其他平台,只能退而求其次选择没有MITM功能但是可以做分流的Clash作为平替了。

Surge的使用方法和现在大多数人采用的Clash使用方法比较不同,具体表现为你必须自己提供一套配置,一般不会有机场提供带有Surge配置的订阅链接,而具体的代理服务器则是由External Group提供的。

Surge上有一个叫做SubStore的模块,具体功能就是管理各种机场订阅,可以对多个订阅的内容进行整合或者简单的处理,配置好Substore后,只需要将其链接设置到Surge的Externel Group里就能使用机场提供的代理了。

但是Clash这边的情况似乎不允许我这样做:

  • 机场提供的都是完整的Clash配置,因此它们无法被写入Clash本地配置文件中的Proxy Provider。
  • 每个机场都会占一个配置文件的位置,没有办法整合在一起。

能不能自己做一个软件,在Clash上实现类似于Surge的使用体验?

思路

我想要的其实就是把多个Clash订阅的代理服务器信息整合到一个里面去并且返回格式符合Clash的Proxy Provider的配置文件,所以只需要做一个Http server,能接收多个订阅地址,然后返回合并后的订阅内容就行了(既然这样那当然也可以选择不合并了)。

实现过程

查找相关信息

相关软件

当然是要先看一下有没有已经可以实现需求的软件,防止重复造轮子。虽然最后没有找到最想要的,但是也找到很有用的一个项目:qier222/proxy-provider-converter: 一个可以将 Clash 订阅转换成 Proxy Provider 和 External Group(Surge) 的工具 (github.com)

这个软件实际上是把订阅链接里面的代理服务器信息提取出来作为Clash的Proxy Provider,所以里面包含了一些我很需要的解析Clash配置文件的部分。

Clash配置文件格式

在他们的官方Wiki中找到了这份说明:Outbound 出站 | Clash (dreamacro.github.io)

Clash还是比较人性化的,Proxy Providers文件其实可以看做是完整配置文件的一部分,这样就让我们要做的工作变得非常简单了:只需要提取出完整配置文件的Proxies部分并且将多个Proxies进行合并并返回。

代码编写

这次我选择使用ASP.NET,一方面是把这当作学习ASP.NET的一个契机,另一方面也是因为我根本没有想过在服务器上部署这个项目,所以就没有选择NodeJS这种对服务器更友好但是对本机不是很友好的方案(这句话经不起推敲,完全是我个人的看法,似乎也说不出什么依据)。

API调用格式

在我的计划中,传入的内容包括两部分:

  • 订阅地址
  • 订阅名字

为什么要订阅名字呢?我是想区分来自不同订阅的节点,同时也可以用来做报错提示。

经过我的观察,订阅地址中很少出现分号,所以我们选择用分号做分隔符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public async Task<IActionResult> GetApiAsync([FromQuery] string urls, [FromQuery] string names)
{
if (string.IsNullOrEmpty(urls))
{
return BadRequest("Missing parameter: urls");
}

if (string.IsNullOrEmpty(names))
{
return BadRequest("Missing parameter: names");
}

try
{
...
var arrUrls = urls.Split(';');
var arrNames = names.Split(";");

for (var index = 0; index < arrUrls.Length; index++)
{
...
}
...
}
...
}

User Agent

最开始在这里卡了很久,没想明白为什么前面提到的proxy-provider-converter能获取到YAML格式的配置但是我只能得到base64,重新看过其源码后才意识到是UA的问题。

不过我直接抄它的UA显得有点不好,所以就看了ClashX的代码,选择使用它的UA。

1
2
HttpClient.DefaultRequestHeaders.UserAgent.Clear();
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ClashX Runtime");

获取订阅文件并提取Proxies

核心功能其实很简单,就是下载配置文件,读取里面的proxies项,然后存到一个总的List里面,等待后面处理并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
try
{
var result = await HttpClient.GetAsync(arrUrls[index]);
result.EnsureSuccessStatusCode();
var configFile = await result.Content.ReadAsStringAsync();

object? config;
using (var reader = new StringReader(configFile))
{
config = YamlDeserializer.Deserialize(reader);
}

if (!(config is IDictionary<object, object> configDictionary) ||
!configDictionary.ContainsKey("proxies"))
throw new Exception("No proxies in this config.");

var yamlResponse = new Dictionary<string, object>
{
{ "proxies", configDictionary["proxies"] }
};

if (yamlResponse["proxies"] is not List<object> listProxies)
continue;

foreach (var proxy in listProxies)
{
if (proxy is not Dictionary<object, object> tempProxy)
continue;

tempProxy["name"] =$"{arrNames[index]} {tempProxy["name"]}";
allYamlResponses["proxies"].Add(tempProxy);
}
}
catch (Exception e)
{
...
}

错误提示

如果没下载成功呢?我希望这个软件是后台运行的,如果在Clash自动更新配置的时候,突然弹出来一个框告诉你获取失败那就太难受了。

所以我想虚构一个代理服务器,用它的名字来提示用户出错,这样在用户自己感知到错误时就可以查看原因,在没有感知到的时候则相当于什么都没发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
catch (Exception e)
{
// exception alert
allYamlResponses["proxies"].Add(new Dictionary<object, object>()
{
{ "name", $"{arrNames[index]} error: {e.Message}" },
{ "type", "ss" },
{ "server", "google.com" },
{ "port", "11111" },
{ "cipher", "chacha20-ietf-poly1305" },
{ "password", "123456" },
{ "udp", "true" },
});
...
}

这里Clash要求必须所有字段都写全,不然它就会弹出错误提示了。

本地文件Fallback

前面提到了出错但是用户无感知,既然用户无感知那就是能正常用。

机场配置更新失败也不是什么罕见的事情,影响因素太多了,而且不更新也不意味着就不能用。如果Clash没获取成功,那它就会继续沿用上一次获取配置时保存的本地文件,我们也需要一个这样的机制。

这里也是我要求用户提供name的一个原因。

1
2
3
// save to local file for fallback
await System.IO.File.WriteAllTextAsync($"./{arrNames[index]}.yml",
YamlSerializer.Serialize(yamlResponse));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
catch (Exception e)
{
...
// fallback to local file
object? localConfig;
if(!System.IO.File.Exists($"./{arrNames[index]}.yml"))
continue;

var localConfigFile = await System.IO.File.ReadAllTextAsync($"./{arrNames[index]}.yml");
using (var reader = new StringReader(localConfigFile))
{
localConfig = YamlDeserializer.Deserialize(reader);
}

if(localConfig is not IDictionary<object, object> localConfigDictionary)
continue;

if (localConfigDictionary["proxies"] is not List<object> listProxies)
continue;

foreach (var proxy in listProxies)
{
if (proxy is not Dictionary<object, object> tempProxy)
continue;

tempProxy["name"] = arrNames[index] + tempProxy["name"];
allYamlResponses["proxies"].Add(tempProxy);
}
}

返回合并后的proxies

1
2
3
var responseContent = YamlSerializer.Serialize(allYamlResponses);
Response.ContentType = "text/plain; charset=utf-8";
return Content(responseContent);

后续改进 (todo)

可能以后会做,如果有人帮我做了更好(真有人看我的博客?)。

基于正则表达式的代理服务器筛选

真的要实现Surge的体验我觉得缺的除了Mitm等等高级功能,就差这个东西了。现阶段想要筛选出不同地区的代理服务器并且放到不同组里还是有待完成的。不过这个功能对于我来说不是非常强的刚需,所以就暂时没做

更先进的订阅管理和订阅组管理

我其实是想做一个Substore翻版,那种有自己的GUI管理界面的,这样看订阅信息、管理订阅组等等都会方便很多,但是由于刚开始没想好,一写就是ASP.NET,导致现在转不过弯来了,这个只能说是后续的后续。

成果和总结

最终在ChatGPT的帮助下和自己不要脸地到处乱抄下实现了预期的功能,软件代码已开源:ParaN3xus/ClashProxiesExtractor: Extract and combine proxies from several subscription url. (github.com)

仓库中还提供了一个Windows的构建和静默自启动的实现方法。

经过这个软件的编写大致掌握了ASP.NET编写简单Http API服务器的方法,也对Clash配置文件有了一些了解。

留言评论

0条搜索结果。