Arch Linux 下 Clash + cgproxy 实现全局代理(透明代理)

2022-12-12 更新

DNS 污染,我解不开,没有相关的知识。最后决定不用全局代理了,为几个必要的、常用的应用单独设置即可。

终端

我用的终端是 zsh ,配置环境变量 ~/.zshrc/.zshenv:

export ALL_PROXY="http://127.0.0.1:7890"

浏览器

我用的是 Vivaldi ,基于 Chromium 内核的,因此只需要设置系统代理就好。

Emacs

Emacs 用到的网络请求主要是 GNUS 和一些自己用 emacs-request 写的函数:

(setq url-proxy-services
      '(("http" . "127.0.0.1:7890")
        ("https" . "127.0.0.1:7890"))

      request-curl-options
      '("-x" "http://127.0.0.1:7890"))

起因

上次没主意把 v2ray 升到了 v5 版本,然后出了大问题。尝试回滚,把 v2ray 装回 4.4 ,然后报了错,连不上站点,日志看起来是服务器的问题,事实上我手机一直用着,所以估计 v2ray 是报废了。之前一直用 qv2ray + cgproxy 做全局代理[1],很方便,每个应用程序都会走代理,省去很多麻烦 – 其实根本就没管过。现在用不了了就考虑折腾一下 clash ,GLaDOS 跟我说 clash 更简单,所以尝试一番。

网上很多教程是配置 iptables ,还考虑什么旁路路由。我实在是不懂,硬着头皮乱配置一通,结果大失败。正心灰意冷之际,突然看到:

ec554c3ecc1255a56b799104caeba05a.png

Figure 1: cgproxy 项目简介

44a15e20e0bad435085963db1dc80c53.png

Figure 2: Clash 项目简介

也就是说,cgproxy是通过 Tproxy 把流量转给 qv2ray 的,而 clash 也支持 Tproxy 。我突然心生一计 ——先睡觉

环境

本次尝试基于 Arch linux ,对于其他系统,如果你要折腾可能要亲力亲为,我爱莫能助。

需要的工具有:

  • clash
  • cgproxy
  • Python3.10, 和一个第三方库 pyyaml 。 这个我是用来更新订阅的,至于大佬们用 curl + sed ,我只能是望尘莫及。

工具安装

Clash 安装

Clash 的下载页面在这里

sudo pacman -S clash

安装成功

clash -v
# Clash 1.11.8 linux amd64 with go1.19 unknown time

然后去安装规则库[2],地址在这里。下载 Country.mmdbrelease分支(Daily) ,然后复制到 ~/.config/clash 下。如果目录不存在手动创建,这个就是后面用来存配置的地方。

cgproxy 安装

cgproxy 的下载页面在这里

yay -S cgproxy

如果 yay 遇到网络问题,可以用[1],这样得先安装 wget

wget https://archlinuxstudio.github.io/ArchLinuxTutorial/res/cgproxy-0.19-1-x86_64.pkg.tar.zst
sudo pacman -U cgproxy-0.19-1-x86_64.pkg.tar.zst

安装成功

cgproxy --help
# Run program with proxy
# Usage: cgproxy [--help] [--debug] <CMD>

Pyyaml 安装

这个库用来解析 yaml 文件, clash 的配置文件就是 yaml 格式。

注意,因为最终这些流程要写成系统服务,到时候执行 Python 脚本会以 root 权限去执行,所以 Python 库的安装不能装在普通用户家目录,也就是要用 root 权限去安装。

sudo pip pyyaml

安装成功

# 用 root 权限执行 python
$ sudo python 
>> import yaml
>> yaml.__version__
'6.0'

Clash 配置

Clash 配置文件

clash 的使用:

clash -d ~/.config/clash

-d 指定了用哪个目录来作为配置文件目录。

如果目录没建的话,它会自动生成,同时会去下载 mmdb 。因为在Clash 安装那里已经装过了,这里就会跳过下载 —— 如果你没装它就自动安装。

配置文件路径就是 ~/.config/clash/config.yaml 。我用的是订阅,下载来文件就是一个配好了的 cofnig.yaml

我是写了另一个配置文件,通过 Python 来处理的,在Python 配置这一节我会说到,这里我介绍一下那些配置。

tproxy-port: 7893

dns:
  enable: true
  enhanced-mode: fake-ip
  listen: 0.0.0.0:53
  nameserver:
    - 119.29.29.29
    - 114.114.114.114
  fallback:
    - https://1.1.1.1/dns-query
    - https://8.8.8.8/dns-query
  • tproxy-port 顾名思义,它指定了 tproxy 的监听端口, cgproxy 获取应用流量之后通过这个端口发给 clash ,这个要和 cgrpxoy 的配置一致(见cgproxy 配置
  • dns.enable 开启 dns 嗅探
  • dns.enhanced-moderedir-hostfake-ip 两种模式[3]
    • redir-host: 传统的DNS转发模式,局域网根据规则查询DNS请求获取真实IP地址。
    • fake-ip: 伪造 IP 模式,内部DNS查询得到的是伪造 IP 段的某个地址,局域网使用伪 IP 与 Clash 通信, Clash 会根据伪 IP 找到真实 IP 使用 SOCKS 代理进行通信。
  • dns.listen dns 监听地址,ip 就填 0.0.0.0 ,端口填 53 会有冲突,下面会调。
  • dns.nameserver dns 服务器
  • dns.fallback 如果GEOIP非 CN 时使用的 dns 服务器[3]

为了使 clash 能监听 dns 的 53 端口,要写改 /etc/systemd/resolved.conf 文件[4]

DNSStubListener=no

2022-12-12 更新

在使用期间经历了一次 DNS 污染,去网上查了资料,需要把 dns 服务器的地址改成 TSL 或 TCP 请求 [5]

tproxy-port: 7893
dns:
  enable: true
  enhanced-mode: fake-ip
  listen: 0.0.0.0:53
  nameserver:
    - 'tls://dns.rubyfish.cn:853'
  fallback:
    - 'tls://1.1.1.1:853'
    - 'tcp://1.1.1.1:53'
    - 'tcp://208.67.222.222:443'
    - 'tls://dns.google'

Clash 服务

把 clash 作为系统服务,让它随系统启动而启动,同时自己设置守护进程,挂了自己重启[6], [7]

脚本编写

创立目录 etc/clash ,写一个脚本 clash.sh ,这个脚本用于控制 clash 的启停。要把 CONFIG_DIR 改成你的路径:

#!/bin/bash

# 配置文件目录
CONFIG_DIR=/home/<you user name>/.config/clash

update() {
    # 检查更新
    python /etc/clash/update.py ${CONFIG_DIR}/env.yaml
}

start() {
    # 开启 IP 转发
    echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf && sysctl -p
    echo "net.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.conf && sysctl -p

    # 启动 cgproxy
    systemctl start cgproxy.service

    # 启动 Clash
    echo "starting Clash"
    echo $$ > /var/run/clash.pid
    /usr/bin/clash -d ${CONFIG_DIR}
}

stop() {
    # 关停 cgproxy
    systemctl stop cgproxy.service

    # 关停 Clash
    PID=`cat /var/run/clash.pid`
    kill -9 ${PID}
    rm /var/run/clash.pid

    # 关闭 IP 转发
    echo "net.ipv4.ip_forward = 0" >> /etc/sysctl.conf && sysctl -p
    echo "net.ipv6.conf.all.forwarding = 0" >> /etc/sysctl.conf && sysctl -p
}

status() {
    echo "TODO"
}

case $1 in
    update)
        update
        ;;
    start)
        start
        ;;
    stop)
        stop
        ;;
    status)
        status
        ;;
    *)
        echo "$0 update | start | stop | status " 
        ;;
esac

写服务脚本 /etc/systemd/system/clash.service

[Unit]
Description=Clash Service
Requires=network-online.target
After=network.target network-online.target

[Service]
ExecStartPre=/etc/clash/clash.sh update
ExecStart=/etc/clash/clash.sh start
ExecStop=/etc/clash/clash.sh stop
Restart=on-failure
RestartPreventExitStatus=23

[Install]
WantedBy=multi-user.target

更新服务:

systemctl daemon-reload # 刷新服务
systemctl start clash.service # 启动服务,目前还不能启动
systemctl enbale clash.service # 服务开机自启动

流程介绍

  • 预处理

    ExecStartPre 调用控制脚本的 update ,在服务正式启动前作处理。控制脚本的 update 其实就是执行了一个 Python 脚本,用来更新订阅(详见 Python 配置)。

  • 启动

    ExecStart 调用控制脚本的 start

    1. 1 写到 ip_forward ,表示要开启 IP 转发。
    2. 启动 cgproxy 服务
    3. 记录当前 clash 服务的 PID ,用于停止服务
    4. 开启 clash ,指定目录
  • 停止

    ExecStop 调用控制脚本的 stop

    1. 停止 cgproxy 服务
    2. 根据启动时记录的 PID 去关闭 clash 进程
    3. 1 写到 ip_forward ,停止 IP 转发

cgproxy 配置

配置文件位于 /etc/cgproxy/config.json

  {
    "comment":"For usage, see https://github.com/springzfx/cgproxy",

    "port": 7893,
    "program_noproxy": ["clash"],
    "program_proxy": [],
    "cgroup_noproxy": ["/system.slice/clash.service"],
    "cgroup_proxy": ["/"],
    "enable_gateway": false,
    "enable_dns": true,
    "enable_udp": true,
    "enable_tcp": true,
    "enable_ipv4": true,
    "enable_ipv6": true,
    "table": 10007,
    "fwmark": 39283
}

各个选项[8]

  • port Tproxy 的端口,就是 clash 配置文件里面指定的 tproxy_port
  • program_noproxy 不走代理的程序,把 clash 排除,不然会流量循环。像我的音乐软件 listen1 就可以加到里面,因所有流量都是国内的,用代理容易出现网络问题
  • program_proxy 走代理的程序,这里留空,在后面程序组那里配置了
  • cgroup_noproxy 不在代理的程序组
  • cgroup_proxy 走代理的程序组, / 表示全部

剩下的就不介绍了,可以参考官网。

到这里,你可以把服务脚本里边的 ExecStartPre=/etc/clash/clash.sh update 先注释掉。然后更新服务,启动 clash ,应该就可以使用了, curl https://google.com 测试一下。

Python 配置

这部分是用来更新订阅的。

先写一个 py 脚本 /etc/clash/update.py [9], [10], [11]

import yaml
import http.client
import traceback
from urllib.parse import urlparse
import time
import sys
import os

def check_update(interval: int) -> bool:
    update_file = "/etc/clash/.lastupdate"
    cur_time = int(time.time())
    need_update = False
    with open(update_file, "r+" if os.path.exists(update_file) else "w") as f:
        s = f.read() if f.readable() else None

        if s:
            last_update = int(s)
            # 非空文件,且超过更新间隔
            if cur_time - last_update >= interval * 3600 * 24:
                need_update = True
        else:
            # 空文件,新建
            last_update = 0
            need_update = True
            f.write(str(cur_time))
    return need_update

def read_env(env_path: str) -> dict:
    with open(env_path) as f:
        s = f.read()
    data = yaml.safe_load(s)
    return data

def download_file(url: str) -> bytes:
    purl = urlparse(url)
    doamin = purl.netloc
    path = purl.path

    data = b''
    try:
        conn = http.client.HTTPSConnection(doamin)
        conn.request("GET", path)
        r = conn.getresponse()
        print('File downloaded.')
        data = r.read()
    except:
        traceback.print_exc()

    return data

def write_and_update_to_file(file_data: bytes, env_data: dict) -> None:
    print('Parsing file')
    try:
        data = yaml.safe_load(file_data)

        # TPORXY-PORT
        data["tproxy-port"] = env_data["tproxy_port"]

        # DNS
        data["dns"]["enable"] = True
        data["dns"]["listen"] = "0.0.0.0:%d" % env_data["dns_port"]
        data["dns"]["nameserver"] = env_data["nameserver"]
        data["dns"]["fallback"] =  env_data["fallback"]
        data["dns"]["enhanced-mode"] = "fake-ip"

        save_path = env_data["clash_config"]
        print("Saving file to file: %s" % save_path)
        with open(save_path, "w") as f:
            f.write(yaml.safe_dump(data))

        print("Update done")
    except:
        traceback.print_exc()

if __name__ == "__main__":
    env_path = sys.argv[1]
    env_data = read_env(env_path)
    if check_update(env_data["interval"]):
            print("Updating")
            write_and_update_to_file(
                download_file(env_data["clash_url"]),
                env_data
            )
    else:
            print("Donnot update")

配置文件 ~/.config/clash/env.yaml

# TPROXY 端口
tproxy_port: 7893

# DNS 监听端口
dns_port: 53

# 订阅地址,一个 yaml 文件
clash_url: https://xxx/yyy.yaml

# 订阅文件路径
clash_config: /home/<your user name>/.config/clash/config.yaml

# 更新间隔(单位:天)
interval: 7

# 内 DNS 服务器
nameserver:
  - 119.29.29.29
  - 114.114.114.114

# 外 DNS 服务器
fallback:
  - https://1.1.1.1/dns-query
  - https://8.8.8.8/dns-query

测试: sudo python /etc/clash/update.py ~/.config/clash/env.yaml

结语

至此,clash + cgproxy 的透明代理就配置完成了。

如果想用 iptables 的话可以去参考文末给出的文章;想用 qv2ray + cgproxy 的话看参考的第一篇[1]

希望对你有帮助

Powered by Org Mode.