Chrome のように話すよう Go プログラムを教える

海外旅行で、教科書どおりの完璧なフレーズでコーヒーを注文したことがあるなら、バリスタが向けてくるあの視線をご存じでしょう。文法は正しく、単語もすべて辞書に載っています。それでも、わずか半秒のうちに、店内の全員があなたが他所から来た人だと見抜いてしまうのです。
Goの標準HTTPクライアントにも、これに似た問題があります。信頼性が高く、簡潔で、箱から出してすぐに本格的な作業に使えるという点で、優れたデフォルトです。しかし、プログラムがサーバーへ接続する際には、それが明らかにGoのプログラムであることが分かってしまいます。ほとんどの場合、それで問題になることはありません。とはいえ、一部のウェブサイト、CDN、テスト環境では、どのようなクライアントが接続してきたかに細心の注意を払っており、相手がどの種類のクライアントだと判断するかによって、振る舞いが大きく異なります。
そこで私たちはgosuda.org/chromenetを開発しました。Goの標準クライアントにブラウザらしい振る舞いを無理に身につけさせるのではなく、chromenetを使えば、Goのプログラムが、Google Chromeと同じネットワークエンジン、つまりChromium自身のネットワークコードを用いてウェブと通信できます。自前のテスト環境がブラウザからのトラフィックをどう扱うかを確認する場合でも、慎重な態度を取るCDNの背後にあるサイトと通信する場合でも、Chromeのエンジンを介して通信することで、推測に頼る部分の大きな層を取り除くことができます。
利用するためにアプリケーションを書き直す必要はありません。コード上では、http.Transportの代わりにそのまま組み込める形で動作します。これまでどおりhttp.Clientを使い続けることができ、その裏側での通信はchromenetが担います。
最新リリースを試すには、go getでインストールしてください。
1go get gosuda.org/chromenet@v0.2.10157081033800
次に、そのトランスポートをクライアントに渡します。
1client := http.Client{
2 Transport: chromenet.NewTransport(chromenet.Options{}),
3}
今回のリリースでは、現代のウェブトラフィック全般がChromeのエンジンを経由するようになりました。通常のウェブリクエストに加え、WebSocketとWebTransportにも対応しています。従来のバージョンでは通常のリクエストのみが適切に処理され、WebSocketとWebTransportは代用的な実装にすぎませんでした。そのため、最初の挨拶こそChromeらしく聞こえても、会話のスタイルを切り替えると、その偽装が剥がれてしまうことがありました。本リリースでは、これらの代用的な実装を本物に置き換えています。
ここで疑問が生じるかもしれません。Chromium自身のネットワークコードを使うのであれば、静かだったGoのビルドが、巨大なC コンパイラの厄介事になるのではないか、と。幸いなことに、そうはなりません。Goのコードは引き続き純粋なGoのままで、cgoも不要であり、Cコンパイラも必要ありません。
chromenetは、ブラウザのコードをバイナリに直接組み込むのではなく、エンジンを小さな別プログラムとしてGoプログラムの隣で動作させます。流暢な現地の通訳者を雇い、その人に自分の声帯を作り直させるのではなく、隣を歩いてもらうようなものだと考えてください。エンジンは別プログラムで動作するため、その内部でクラッシュが起きても、あなたのサービス全体が停止することはありません。

Goプログラムは純粋なGoのまま、小さな補助プログラムがChromiumのネットワークエンジンを通じてトラフィックを運びます。
また、開発用のワークステーションやサーバーに追加でインストールするものもありません。Linux、macOS、Windowsの各OS向けに、Intel/AMDおよびARMの両方に対応したエンジンのバイナリが、Goモジュールの内部に同梱されており、実行前に検証されます。
モジュール内にバイナリを同梱すると、信頼性を重視する方々にとっては当然ながら疑問が生じます。私たちはそれに対して率直な答えを用意したいと考えました。バイナリは、特定のChromiumリリース(現在はChromium 157)から再現可能な方法でビルドされており、誰でも再ビルドして全く同一のバイト列を得ることができます。また、各リリースはGitHubによって証明(attestation)されています。ブラウザの世界に手作業なしで追従するため、定期実行されるジョブがChromiumの安定版チャンネルを追跡し、新しいChromeがリリースされるたびにバージョンを自動的に更新します。
uTLSについて
以前、Goのクライアントをブラウザらしく見せようとしたことがあるなら、おそらくuTLSに出会っているでしょう。これは見事な成果物です。安全な接続の最初のメッセージを、Chrome、Firefox、その他お好みのブラウザに似せて装わせることのできるGoライブラリです。小規模で純粋なGoによるものであり、プログラムの隣で余計なものが動作することもありません。
ただし、その装いが及ぶ範囲には限界があります。uTLSがカバーするのは、会話の最初の数語にあたるハンドシェイクだけです。それ以降はすべて自分で組み立てる必要があります。HTTP/2層は手作業で接続し調整しなければならず、HTTP/3は別の大仕事になり、WebSocketやWebTransportを扱うにはさらに多くの接着コードが必要になります。また、Chromeのプリセットは複製されたものであり、Chromeの振る舞いが変わるたびに、誰かがそれを最新に保たなければなりません。それは、相手の訛りを真似ても、体の動きから正体が見抜かれてしまうのに似ています。
chromenetは逆のアプローチを取ります。Chromeを模倣するのではなく、Chrome自身のエンジンに通信を任せます。そのため、ハンドシェイク、接続の再利用の仕方、プロトコルのアップグレード、トランスポートのすべてが一つの場所から提供され、Chromiumが変化すると一緒に変化します。
トレードオフについては正直に述べます。uTLSは軽量で、想像しうるあらゆる挨拶を自ら組み立てられます。一方chromenetは、25 MBの補助プログラムを必要とし、独自の振る舞いではなく、Chromeの振る舞いそのものを提供します。依存関係を極力小さくし、ハンドシェイクを完全に制御したい場合には、uTLSが依然として優れた選択肢です。
あらゆる設計にはトレードオフがあり、ここでは限界についてもあらかじめ明らかにしておきたいと思います。本物のブラウザのネットワークエンジンを組み込むことは、決して小さくはありません。各エンジンのバイナリは約25 MBです。macOSでは、バイナリは公証(notarize)されていません。そしてLinuxでは、システムにglibc 2.31以降が必要です。これらの条件がご自身の環境に合うのであれば、トランスポートのフィールドを一つ差し替えるだけで、本物のChromeのネットワーキングを利用できます。
プロジェクトはgithub.com/gosuda/chromenetにあります。gosudaオープンソースファミリーの姉妹プロジェクトであるgosuda.org/hqqやgosuda.org/sqlite3をご存じであれば、その精神はおなじみのものに感じられるでしょう。より広いソフトウェアの世界にある高機能で重厚な仕組みを取り入れ、それを純粋なGoの中で自然に使えるようにする、という考え方です。
そのため、もしGoのプログラムをブラウザらしく振る舞わせる必要があってuTLSに手を伸ばそうとしているなら、ぜひchromenetを試してみてください。特にHTTPSリクエスト以上のものが必要な場合、uTLSの良い代替手段になり得ます。HTTP/2、HTTP/3、WebSocket、WebTransportのすべてが同じChromeエンジンを経由し、利用者側で接着コードを書く必要はありません。