给网站加上订阅网址转换

title image

背景

最近给自己的 secnone.com 顺手加了一个订阅网址转换服务,地址放在 sub.secnone.com

缘起很简单,我之前用 V2rayU, 但是不迭代了,只能改用 clash verge. 结果有些免费订阅源直接丢给 Clash Verge 并不好用,要么格式不认,要么里面混了 VLESS, Hysteria2, Trojan 这些协议。我需求就是把一个原始订阅 URL 转成 Clash 可以直接导入的 YAML。

最开始我想得比较复杂,准备在 secnone-server 这个 Express app 里加一个 /proxy-sub route,先把订阅拉下来,做 Base64 decode, protocol filter, dedupe, limit,然后再交给 subconverter。后来发现压根不需要。

开始的方案

旧方案是这样的:

Clash Verge
  -> https://sub.secnone.com/sub?target=clash&url=...
  -> nginx
  -> Docker subconverter
  -> 原始订阅源

subconverter 本身是一个挺经典的工具,我先用 Docker 跑在服务器上, 然后用 nginx 把公网的 https://sub.secnone.com/sub 反代到本地的 127.0.0.1:25500

这个对一些旧格式订阅是能工作的,但有些源处理不了, 直接走 subconverter 经常返回报错:

No nodes were found!

问题主要出在 subconverter 对 plain text subscription, Base64 wrapper 和新协议支持不够。

中间绕了一下

我一度准备给我的网站 Express 加一层 proxy-sub

subconverter
  -> secnone-server /proxy-sub
  -> fetch 原始订阅
  -> decode
  -> filter
  -> 返回纯文本节点

这个方案有点费力不讨好,为了一个订阅转换功能,把 SSRF 防护、redirect 校验、timeout、response size limit 都塞进主站 Express,维护很麻烦。

后来试了 Sublink Worker,发现它可以直接处理 HTTP/HTTPS subscriptions,也支持 VLESS, Hysteria2, Trojan。就它了。

项目地址:7Sageer/sublink-worker

最后架构变成:

Clash Verge
  -> https://sub.secnone.com/clash?config=...
  -> nginx
  -> Docker Sublink Worker
  -> 原始订阅源

接口为:

https://sub.secnone.com/clash?config=<encoded_url>

我用的是 Docker Compose。因为这个服务会开放给大家用,生产配置里和机器相关的细节就不完整贴了,关键点是:worker 只监听本机端口,外面统一走 nginx HTTPS,Redis 只作为 worker 的缓存依赖。

services:
  worker:
    image: ghcr.io/7sageer/sublink-worker:latest
    ports:
      - "127.0.0.1:8787:8787"
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

端口只绑定在 127.0.0.1:8787,公网不直接暴露 Docker 端口,外面只走 nginx HTTPS。这样主站 Express 不需要关心订阅转换逻辑,sub.secnone.com 只负责这一类转换请求。

nginx 配置

sub.secnone.com 的 HTTPS server 里核心反代类似于:

server {
    server_name sub.secnone.com;

    location / {
        proxy_pass http://127.0.0.1:8787;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # HTTPS 证书部分交给 certbot 生成和维护。
}

一个小坑是不能只配:

location = /sub {
    proxy_pass http://127.0.0.1:8787;
}

因为 Sublink Worker 的接口是 /clash, /surge, /singbox 这些 path。location = /sub 只会匹配 /sub,新的接口根本不会进去。所以这里要用 location / 反代整个 sub.secnone.com

最终使用方式

网站已经配置好,未来大家配置梯子的 url 直接用如下即可:

https://sub.secnone.com/clash?config=<原始订阅 URL 做 URL encode>
Written on