Surge で Geosite を使う

2024年9月13日金曜日 / , , ,
簡体中文日本語
要約:
Surge では Geosite のルールセットを直接使えません。Cloudflare Worker で都度変換するサービスを作り、正規表現もできるだけ DOMAIN-WILDCARD に変換しました。

Surge は Geosite のルールセットを直接利用できません。ドメインのマッチングにはワイルドカードなどのルールを使うため、網羅的な Geosite のルールセットを使いたければ、まず Surge が対応する形式に変換する必要があります。

GitHub には、GitHub Actions で Geosite のルールセットを Surge 向けに定期変換するリポジトリが数多くあります。ただし、定期実行なので Geosite が更新されてもすぐには反映されず、常に上流より遅れてしまいます。また、私が確認したいくつかのリポジトリでは正規表現ルールをそのまま捨てており、本来マッチするはずのドメインが大量に対象から漏れていました。

そこで、Cloudflare Worker を使って、上流のルールセットをリアルタイムに変換しつつ楽観的にキャッシュするサービスを作り、正規表現ルールもできるだけ多く処理して、ルールを取りこぼさないようにすることにしました。

そして、xxxbrian/Surge-Geosite が誕生しました。公開サービスは surge.bojin.co にデプロイしています。

使い方

Geosite のリスト名をパスの後ろに付け、絞り込みが必要なら続けて @属性 を付けます。

TEXT
https://surge.bojin.co/geosite/<列表名>[@<属性>]

apple は Apple の全ドメインを対象とし、apple@cn は上流で @cn が付いている部分だけを残します。Surge ではそのまま RULE-SET として使用できます。

INI
[Rule]
RULE-SET,https://surge.bojin.co/geosite/apple@cn,DIRECT
RULE-SET,https://surge.bojin.co/geosite/category-ads-all,REJECT
RULE-SET,https://surge.bojin.co/geosite/openai,Proxy,update-interval=3600

利用可能なリスト名の一覧は surge.bojin.co/geosite から JSON 形式で取得できます。

実装について

Surge のドメインルールでは DOMAIN、DOMAIN-SUFFIX、DOMAIN-KEYWORD、DOMAIN-WILDCARD を使用し、さらに RULE-SET でリモートリストを参照できます。

一方、Geosite のルールセットでは domain:、full:、keyword:、regexp:、include: を使用し、ルールの末尾には例えば @cn を付けて特定の地域を絞り込むこともできます。

最初の数種類は一対一で直接変換できますが、regexp: と include: は少し厄介です。

一対一で対応するルール

Geosite

Surge

domain:a.com(プレフィックスを書かない場合もこの種類です)

DOMAIN-SUFFIX,a.com

full:a.com

DOMAIN,a.com

keyword:abc

DOMAIN-KEYWORD,abc

実装では、各行のプレフィックスを判定し、対応する Surge のルールに置き換えて出力しているだけです。行末の @cn とコメントはそのまま # 注释 としてルールの後ろに残します。これにより、Surge で特定のルールを見たときに、そのルールが上流でどの属性を持っているかを知ることができます。たとえば apple を変換すると、だいたい次のようになります。

TEXT
# include:apple-dev
# include:swift
DOMAIN-SUFFIX,swiftui.cn # @cn
DOMAIN-SUFFIX,swiftui.com.cn # @cn

include

include の処理は再帰的です。Worker は include:xxx を見つけると、raw.githubusercontent.com から対応するリストを取得し、同じロジックで変換してから元の位置に貼り付け、その上に # include:xxx を目印として 1 行残します。こうして最終的に完全なリストが出力され、Surge は上流に include という関係があることを意識する必要がありません。

Note

include ごとに 1 回のリクエストが発生するため、大きなリストは初回アクセスが少し遅くなります。変換後の結果は Hono の cache ミドルウェアで 30 分間キャッシュしており、その後のリクエストはキャッシュに直接ヒットします。ここでいうリアルタイムとは、上流の更新から約 30 分でサービスに新しいルールが反映されるということです。実際に Surge に届くまでには、さらに Surge 自身の更新間隔も加わります。1 日 1 回の Action と比べれば、これでもかなり速くなっています。

正規表現

Surge のドメインルールには regex タイプがなく、最も近いのは DOMAIN-WILDCARD ですが、DOMAIN-WILDCARD はワイルドカード方式でマッチングし、* と ? しか使えません。そこで、複雑な正規表現をなるべく * に置き換える方法を考えました。

まず上流の正規表現を集計しました。当時、全部で 1283 個のリストがあり、regexp: は 208 行あり、そのうち category-porn が 134 行、qcloud が 29 行、apple が 6 行で、残りは比較的分散していました。純粋なリテラルは 1 つもなかったため、そのまま DOMAIN または DOMAIN-SUFFIX に直接置き換えられるものはありませんでした。多くは次のような形です。この 3 つは apple に含まれていました。

TEXT
regexp:^a[1-5]\.mzstatic\.com$ @cn
regexp:^cl[1-5]\.apple\.com$ @cn
regexp:^s[1-5]?\.mzstatic\.com$ @cn

これらは番号の異なる CDN ホスト名の一群を表しており、a1 から a5 のようなもので、ワイルドカードでしか表現できません。

最初は regexp: をそのまま DOMAIN-KEYWORD に置き換え、正規表現をそのまま入れてしまいましたが、後になって DOMAIN-KEYWORD は単なる部分文字列マッチングであり、^、[、( といった文字はホスト名にはそもそも現れず、変換されたルールが決してヒットすることはないと気づきました。

文字列置換も考えましたが、グループ、分岐、文字クラス、量詞、エスケープは相互にネストでき、1 文字ずつ処理すると括弧の扱いで間違えやすいため、最終的には regjsparser で正規表現を構文木に解析し、ノードの種類ごとに出力方法を決めることにしました。

TypeScript
import { parse } from "regjsparser";

const convertNodeToWildcard = (node): string => {
  switch (node.type) {
    case "alternative": return node.body.map(convertNodeToWildcard).join("");
    case "anchor":      return "";   // ^ 和 $
    case "disjunction": return "*";  // a|b
    case "quantifier":  return "*";  // x* x+ x? x{m,n}
    case "reference":   return "*";  // 反向引用
    case "group":       return convertNodeToWildcard(node.body[0]);
    case "dot":
    case "characterClass":
    case "characterClassEscape":
    case "unicodePropertyEscape": return "?"; // 恰好一个字符
    case "value":       // 字面字符
      return typeof node.codePoint === "number" ? String.fromCodePoint(node.codePoint) : "?";
  }
};

ここでは戻り値が同じいくつかの分岐をまとめています。ルールをまとめると、リテラル文字はそのまま残し、^ と $ は直接削除します。ワイルドカードはもともと全体一致だからです。また、.、[a-z]、\d のように、ちょうど 1 文字に一致するものは ? に変換します。*、+、?、{m,n} といった量指定子は * に変換します。選択 a|b は表現できないので、同じく * に変換します。括弧内は再帰的に処理します。

このルールに従うと、上の 3 つは次のように変換されます。

TEXT
a?.mzstatic.com
cl?.apple.com
s*.mzstatic.com

正規表現の解析に失敗すると、変換関数は空文字列を返すため、不完全な行 DOMAIN-WILDCARD, が出力されます。Surge はルールセット内の無効な行をスキップして警告を出すだけで、リスト全体には影響しません。そのため、追加の処理は入れていません。

Warning

こうして変換したルールは、元の正規表現よりマッチ範囲が広くなります。例えば [1-5] を ? に変えると、a?.mzstatic.com は a9.mzstatic.com にも一致します。大多数を占める (^|\.) で始まるものも同様です。選択全体が * に置き換わるため、(^|\.)nnbw[0-2]\.xyz$ は *nnbw?.xyz になります。本来は nnbw が文字列の先頭かドットの直後に来る必要がありましたが、変換後は xnnbw0.xyz にも一致します。もともとの目標はルールを取りこぼさないことです。似たドメインを多少余分にマッチさせるほうが、ルールを丸ごと捨てるよりはよいので、この点は受け入れています。

Action との比較

Action による定期変換と比べると、更新遅延は 1 日から 30 分程度に短縮されました。正規表現も直接破棄せず、DOMAIN-WILDCARD に変換しています。リストと属性の組み合わせは事前に生成する必要がなく、apple@cn のようなリクエストが届いてから変換されるため、欲しいものをその場で選べます。さらに、生成物は GitHub 上に置かず、Cloudflare のエッジノードから直接配信されるので、GitHub へのアクセスが不安定になる心配もありません。

サービス全体のコードは多くないので、興味があればリポジトリを直接見てください。リポジトリには 1 日 1 回実行される Action もあり、上流の submodule の更新と README 内のリスト目次の再生成だけを担当しています。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...