【树莓派学习笔记】Clash配置自动编辑

代理服务提供商提供的clash配置不能满足笔者的需求,而每次更新配置后如果手动编辑配置文件又显得有些麻烦。 经过一番痛苦的挣扎之后笔者决定将整个过程自动化。

前情提要

之前的文章中笔者搭建了局域网代理服务器,使用一个systemd守护clash,并且自动在服务启动时更新clash配置文件。
其大致流程如下所示。

graph TB;
    A[从服务商链接获取最新的config.yaml]
    B[编辑allow-lan字段]
    C[启动clash]
    A-->B;
    B-->C;

之前的服务中已经对配置文件进行了简单的编辑,主要就修改了一个allow-lan字段和log-level字段,因为咱们需要最详细的日志信息和允许局域网连接。

#!/bin/bash
# save this file to ${HOME}/.config/clash/start-clash.sh

# save pid file
echo $$ > /etc/clash/clash.pid
config=/etc/clash/config.yaml
diff $config <(curl -s ${CLASH_URL})
if [ $? != 0 ]; then
    curl -L -o $config ${CLASH_URL}
fi

allow_lan=`sed -n '/allow-lan/=' ${config}`
sed -i "${allow_lan}a allow-lan: true" ${config}
sed -i "${allow_lan}d" ${config}

log_level=`sed -n '/log-level/=' ${config}`
sed -i "${log_level}a log-level: debug" ${config}
sed -i "${log_level}d" ${config}

/usr/local/bin/clash -d /etc/clash/

自动切换节点

官方提供的clash配置文件提供了一个自动切换的代理节点组,但是官方把所有的节点都列进去了,所以自动切换是在所有节点之间切换。
而笔者需要一个只包含美国节点的自动切换组。

Python编辑YAML

仍然使用之前的clash服务启动流程,在修改配置文件这一步引入更复杂的逻辑并使用python实现。

PyYAML库提供了读写yaml文件的字典抽象,因此可以使用python轻易编辑yaml而不用像之前bash一样使用正则表达式匹配。

实现结果

这里列出关键函数,其余部分笔者已开源
其中的函数get_config函数通过系统环境变量获取clash配置文件的下载链接,下载原始配置文件后转化为一个字典返回。

def edit_config(input_file, output_file, do_update=True):
    if do_update:
        data = get_config()
        if data:
            pass
        else:
            print('Update fail, check internet connection or config link.')
            raise ConnectionError(f"Connection error or invalid link.")
            # return
    else:
        # read from YAML file
        with open(input_file, 'r') as file:
            data = yaml.safe_load(file)
            file.close()

    # default proxy group
    default_proxy_group = {
        "name": "default",
        "type": "url-test",
        "url": "http://www.gstatic.com/generate_204",
        "interval": 300,
        "proxies": []
    }

    # edit data
    ## create a auto switch group for United States proxies
    proxy_names = [proxy['name'] for proxy in data['proxies']]
    US_proxy_names = [name for name in proxy_names if 'UnitedStates' in name]
    
    US_proxy_group = dict(default_proxy_group)
    US_group_name = 'US-Auto'
    US_proxy_group['name'] = US_group_name
    US_proxy_group['proxies'] = US_proxy_names

    assert 'proxy-groups' in data
    assert type(data['proxy-groups']) is list
    # if type(data['proxy-groups']) is dict:
    #     data['proxy-groups'] = [ data['proxy-groups'] ]
    proxy_groups_names = [group['name'] for group in data['proxy-groups']]

    try:
        i = proxy_groups_names.index('Proxy')
        data['proxy-groups'][i]['proxies'].insert(0, US_group_name)
    except ValueError:
        select_proxy_group = dict(default_proxy_group)
        select_proxy_group['name'] = 'Proxy'
        select_proxy_group['type'] = 'select'
        select_proxy_group.pop('url', None)
        select_proxy_group.pop('interval', None)
        select_proxy_group['proxies'] = proxy_groups_names
        select_proxy_group['proxies'].insert(0, US_group_name)
        data['proxy-groups'].append(select_proxy_group)

    try:
        i = proxy_groups_names.index(US_group_name)
        data['proxy-groups'][i] = US_proxy_group
    except ValueError:
        data['proxy-groups'].insert(0, US_proxy_group)

    ## change allow lan
    data['allow-lan'] = True
    ## chane log level
    data['log-level'] = 'debug'
    
    # 将修改后的数据写回文件
    with open(output_file, 'w') as file:
        yaml.dump(data, file)
        file.close()

然后修改systemd调用的启动脚本,记得将python脚本放到/etc/clash/update.py

#!/bin/bash
# save this file to ${HOME}/.config/clash/start-clash.sh

# save pid file
echo $$ > /etc/clash/clash.pid
config=/etc/clash/config.yaml
py=/etc/clash/update.py

/usr/bin/python3 $py --output /etc/clash/config.yaml --update

/usr/local/bin/clash -d /etc/clash/

Clash配置自动编辑服务

上面的配置放到Linux上都非常方便,systemd真的好使。
但对于Windows用户来说就比较麻烦了,每次可能还得需要手动运行脚本。

Flask解决方案

好消息是,笔者的树莓派与Windows笔记本在同一个蒲公英子网内,所以可以在树莓派上搭建一个网络服务来实现自动化的更新。
服务通过url访问并在访问时以参数形式传入代理服务商提供的链接,然后在树莓派上获取配置并编辑,最后返回给用户。
例如如果用户访问下面的url:

http://raspberrypi.local:6789/?url=https://some.url

则服务器将从https://some.url下载文件,修改后发送给用户。

在询问ChatGPT之后,决定使用flask来实现功能。

Flask服务和更安全的Gunicorn

基于之前的脚本修改主要功能。假设服务器域名为raspberrypi.local(这个其实是笔者树莓派的局域网域名),服务监听端口为6789。

GPT给出的服务器代码如下。

from flask import Flask, request, send_file
import subprocess
import os

app = Flask(__name__)

@app.route('/')
def get_config():
    url = request.args.get('url')
    if url:
        # 调用 get_config.py 脚本
        subprocess.run(['python', 'main.py', '--url', url, '--output', 'output.yaml'])
        # 确保文件已生成
        if os.path.exists('output.yaml'):
            status = send_file('output.yaml')
            subprocess.run(['rm', 'output.yaml'])
            return status
        else:
            return "Failed to generate output file", 500
    else:
        return "No URL provided", 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=6789)

这里由于我给GPT的提示词中提到一个已存在的脚本可以直接调用,所以GPT是以系统调用程序的形式来调用脚本,而不是通过调用函数的方式服用代码。
测试没有问题,但GPT提到了一个安全问题,所以笔者又安装了Gunicorn。

使用Gunicorn的服务启动脚本如下。

gunicorn -w 4 -b 0.0.0.0:6789 server:app

这表示启动了4个工作进程,监听6789端口。

随后将python脚本、bash启动脚本等内容放到/etc/clash-config路径下,并编写service文件。

[Unit]
Description=Clash config daemon
After=network.target

[Service]
Type=simple
User=root
Restart=always
ExecStart=/etc/clash-config/start.sh

[Install]
WantedBy=multi-user.target

加入到systemd启动序列不谈。

使用方法

对于Clash for Windows用户而言,可以从一个url获取配置文件。
只需要填入下面的链接就可以自动获取。

http://raspberrypi.local:6789/?url=https://clash.config.yaml

总结

自动化修改clash配置文件是一个长久以来的需求,但一直没有足够的动力写复杂的逻辑。后来由于GPT的使用,我发现如果不用美国的节点可能会导致GPT访问不稳定,从而有了这样一个具体而强烈的需求。
现在只要一台设备能访问到我的树莓派,那就可以直接从树莓派获取编辑后的clash配置文件。

参考

Gunicorn