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: は少し厄介です。一対一で対応するルールGeositeSurgedomain:a.com(プレフィックスを書かない場合もこの種類です)DOMAIN-SUFFIX,a.comfull:a.comDOMAIN,a.comkeyword:abcDOMAIN-KEYWORD,abc実装では、各行のプレフィックスを判定し、対応する Surge のルールに置き換えて出力しているだけです。行末の @cn とコメントはそのまま # 注释 としてルールの後ろに残します。これにより、Surge で特定のルールを見たときに、そのルールが上流でどの属性を持っているかを知ることができます。たとえば apple を変換すると、だいたい次のようになります。TEXTコピー# include:apple-dev # include:swift DOMAIN-SUFFIX,swiftui.cn # @cn DOMAIN-SUFFIX,swiftui.com.cn # @cnincludeinclude の処理は再帰的です。Worker は include:xxx を見つけると、raw.githubusercontent.com から対応するリストを取得し、同じロジックで変換してから元の位置に貼り付け、その上に # include:xxx を目印として 1 行残します。こうして最終的に完全なリストが出力され、Surge は上流に include という関係があることを意識する必要がありません。Noteinclude ごとに 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 内のリスト目次の再生成だけを担当しています。
Surge は Geosite のルールセットを直接利用できません。ドメインのマッチングにはワイルドカードなどのルールを使うため、網羅的な Geosite のルールセットを使いたければ、まず Surge が対応する形式に変換する必要があります。
GitHub には、GitHub Actions で Geosite のルールセットを Surge 向けに定期変換するリポジトリが数多くあります。ただし、定期実行なので Geosite が更新されてもすぐには反映されず、常に上流より遅れてしまいます。また、私が確認したいくつかのリポジトリでは正規表現ルールをそのまま捨てており、本来マッチするはずのドメインが大量に対象から漏れていました。
そこで、Cloudflare Worker を使って、上流のルールセットをリアルタイムに変換しつつ楽観的にキャッシュするサービスを作り、正規表現ルールもできるだけ多く処理して、ルールを取りこぼさないようにすることにしました。
そして、xxxbrian/Surge-Geosite が誕生しました。公開サービスは surge.bojin.co にデプロイしています。
使い方
Geosite のリスト名をパスの後ろに付け、絞り込みが必要なら続けて @属性 を付けます。
apple は Apple の全ドメインを対象とし、apple@cn は上流で @cn が付いている部分だけを残します。Surge ではそのまま RULE-SET として使用できます。
利用可能なリスト名の一覧は 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 を変換すると、だいたい次のようになります。
include
include の処理は再帰的です。Worker は include:xxx を見つけると、raw.githubusercontent.com から対応するリストを取得し、同じロジックで変換してから元の位置に貼り付け、その上に # include:xxx を目印として 1 行残します。こうして最終的に完全なリストが出力され、Surge は上流に include という関係があることを意識する必要がありません。
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 に含まれていました。
これらは番号の異なる CDN ホスト名の一群を表しており、a1 から a5 のようなもので、ワイルドカードでしか表現できません。
最初は regexp: をそのまま DOMAIN-KEYWORD に置き換え、正規表現をそのまま入れてしまいましたが、後になって DOMAIN-KEYWORD は単なる部分文字列マッチングであり、^、[、( といった文字はホスト名にはそもそも現れず、変換されたルールが決してヒットすることはないと気づきました。
文字列置換も考えましたが、グループ、分岐、文字クラス、量詞、エスケープは相互にネストでき、1 文字ずつ処理すると括弧の扱いで間違えやすいため、最終的には regjsparser で正規表現を構文木に解析し、ノードの種類ごとに出力方法を決めることにしました。
ここでは戻り値が同じいくつかの分岐をまとめています。ルールをまとめると、リテラル文字はそのまま残し、^ と $ は直接削除します。ワイルドカードはもともと全体一致だからです。また、.、[a-z]、\d のように、ちょうど 1 文字に一致するものは ? に変換します。*、+、?、{m,n} といった量指定子は * に変換します。選択 a|b は表現できないので、同じく * に変換します。括弧内は再帰的に処理します。
このルールに従うと、上の 3 つは次のように変換されます。
正規表現の解析に失敗すると、変換関数は空文字列を返すため、不完全な行 DOMAIN-WILDCARD, が出力されます。Surge はルールセット内の無効な行をスキップして警告を出すだけで、リスト全体には影響しません。そのため、追加の処理は入れていません。
こうして変換したルールは、元の正規表現よりマッチ範囲が広くなります。例えば [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 内のリスト目次の再生成だけを担当しています。