blog.anqou.net
rss
author
tags

自前 ActivityPub 実装と Mastodon との E2E テストをローカルでやり切る

ActivityPub(AP)に対応したソフトウェアを独自に実装する際、Mastodon などの他の実装との連合をどのようにテストするかはよく知られた難題です[1]

今回、独自 AP 実装である Waq の E2E テストをローカルで完結させることに成功したので、ここで紹介したいと思います。大筋としては以下のようになっています:

  1. KinD を使って Kubernetes 環境を立ち上げる。
  2. cert-manager・trust-manager で自己署名証明書を生成する。
  3. SSL_CERT_FILE 環境変数に ↑ で生成した証明書を突っ込んだ状態で Mastodon・Waq を(kind 上で)立ち上げる。
    • この段階で、KinD の中で Mastodon・Waq は互いに疎通可能になる。
  4. 生成した TLS 証明書を KinD 外にダンプする。
  5. KinD 上に SOCKS5 サーバを立ち上げる。
  6. export した TLS 証明書を SSL_CERT_FILE 環境変数に突っ込み、さらに kneesocks を使って SOCK5 サーバを経由することで、KinD 外から Mastodon・Waq にアクセスする。
    • これによって E2E テストを行うプログラム自体は KinD 外に置くことができる。

実際に動いているコードは Waq の GitHub のレポジトリの e2e ディレクトリにあります。

#モチベーション

AP で外部と通信するようなソフトウェアを書いていると、他の実装との通信が正しく動作するかどうかはかなり重要な関心事です。特に Fediverse では Mastodon が覇権を握っているので、Mastodon と自分の実装が正しく連合できるかどうかは自動テストによって常に確認しておきたくなります。

一方で、テストのために各実装を正しく連合させた環境を構築するのはかなり面倒なことがよく知られています。理由は大きく 2 つあります:

前者は Docker を使うことによって比較的簡単に解決が可能です。特に Docker Compose や KinD(Kubernetes in Docker)を使うと PostgreSQL や Redis と一緒に Mastodon を立ち上げることができ、自動テストにも便利です。

問題は後者です。一番簡単な方法としては、各サーバの前段に ngrok や Cloudflare Tunnel といったトンネリングツールをかませるというやり方が知られています。ただ ngrok は無料で使える範囲にかなり厳しい制約がありますし、Cloudflare Tunnel は自前のドメインを持っていないと使えません[3]。またどちらの方法でも、サーバがインターネットから誰でも接続できる状況になってしまうため、セキュリティ的に不安が残ります。

そこで、どうにかしてトンネリングの仕組みなしに HTTPS の通信を可能にする仕組みが必要になります。

#自己署名証明書を SSL_CERT_FILE 環境変数に指定してローカルで HTTPS 通信する

HTTPS の通信をするだけであれば、自己署名証明書を作成し使うというのはよくある手です。しかし、単にこのような証明書を使うだけだと証明書の検証が失敗してしまいます。実際、Mastodon も自己署名証明書を提示されると通信を拒絶します。

そこで Mastodon を起動する際に SSL_CERT_FILE 環境変数を自己署名証明書(正確にはその CA の証明書)に向けて設定します。この環境変数は、OpenSSL が信用する証明書を切り替えるためのエスケープハッチになっているため、これを書き換えることで証明書の検証をパスさせることができます[4]

なお証明書を発行する際にはドメイン名が必要になるため[5]、適当な場所に DNS サーバを立てる必要があります。KinD などを使うと Kubernetes が勝手に CoreDNS を立ててくれるため便利です。CoreDNS の設定をいじると(.local ではなく)まともな top-level domain(.net とか)を使うこともできます。

#プライベート IP アドレスから Mastodon への通信を許可する

Docker Compose を使うにしても Kubernetes を使うにしても、Mastodon への通信はプライベート IP アドレスからやってくることになります。Mastodon はデフォルトではプライベート IP アドレスからの通信を拒否するため、そのままでは動きません。

そこで、Mastodon を立ち上げる際に ALLOWED_PRIVATE_ADDRESSES 環境変数を指定することでこの制限を回避します。この環境変数は値に CIDR を受け取ります。例えば KinD で立ち上げた Kubernetes であれば ALLOWED_PRIVATE_ADDRESSES=10.0.0.0/8 を指定しておけば用をなします[6]

#KinD を使って Mastodon(と Waq)を動かす

ここから説明がすごく雑になる上、Kubernetes の知識を仮定します。詳細は Waq のコードを見てください。

以上のような仕組みが自動的に動いてテスト環境が立ち上がるようにするにはコンテナを使うのが便利です。Docker Compose を使ってもよいですが、Kubernetes を使うと何かと便利なので Waq ではそうしています。具体的には KinD を使い軽量な Kubernetes の環境を立ち上げます。

まず CoreDNS の設定を変更して、適当なドメイン名を Mastodon 用の Service に向けておきます。具体的には kube-system namespace にある coredns という名前の ConfigMap を書き換え、rewrite プラグインを呼び出すようにしておきます。

自己署名証明書の発行には cert-manager と trust-manager を使います。ドキュメントの通りに Certificate リソースを作ると、Secret として証明書が作成されます。これを指定して Bundle リソースを適当に作ると、自己署名証明書の中身と、普通の(ca-certificates とかに入っている)証明書が一つにまとまったものが ConfigMap として作成されます。この ConfigMap を Pod 中にマウントした上で SSL_CERT_PATH に指定することになります。

Mastodon の立ち上げには拙作の Mastodon 用 operator である Magout を使います。適当に values.yaml で値をセットした後 Helm コマンドを打つと Mastodon サーバが立ち上がります。

#KinD の外から E2E テストを走らせる

E2E テストを KinD の中で動かすのは面倒なので、このテストプログラム自体は KinD の外から動かします。テストプログラムから Mastodon や Waq のエンドポイントにアクセスするためには (i) KinD の外からこれらのドメイン名を解決しつつ接続し、(ii) TLS 証明書の検証を成功させる必要があります。

まず (ii) は簡単で、trust-manager に作らせた ConfigMap の中身を KinD の外にダンプし、これをテストプログラム起動時に SSL_CERT_PATH で読み込ませることで実現できます。

厄介なのは (i) です。KinD の外では CoreDNS の効果が無いので、なんとか KinD の内側で DNS 解決を行う必要があります。そこで、まず KinD の中に SOCKS5 サーバを立てます。これはどのようなやり方をしてもよいですが Waq では serjs/go-socks5-proxy を使っています。続いて、この SOCKS5 のポートを KinD の外側に露出させます。これには kind port-forward が使えます。その上で、テストプログラムからの全ての通信をこの SOCKS5 サーバ経由で行うようにします。Waq では kneesocks を使ってこれを実現しています[7][8]

#まとめ

外部のトンネリングサービスへの依存から Waq の E2E テストが脱するのは開発を始めた当初からの悲願だったので、やっと達成できてかなり嬉しい気持ちです。みなさんもローカルで自動テストを完結させて良い AP 実装ライフをお過ごしください。後半はかなり駆け足になってしまいましたが、なにか質問があれば @[email protected] までどうぞ。

#注釈

  1. 以前は Mastodon の開発ドキュメントに連合のテストの方法をまとめたページがあった気がしたのですが、今探すと見つかりませんでした。代わりに W3C Wiki に似たようなことが書いてありました。あと ActivityPub まとめ wiki にも「動作確認」というページがあり、知見がまとめられています。

  2. RAILS_ENV=development だと HTTP でも動いた気もするんですが、遠い過去の記憶なので忘れました。とりあえずこの記事では HTTPS 必須ということにします。ちなみに Elk のように HTTPS 以外でつなぐインターフェイスがそもそも用意されてないクライアントもあるので、HTTPS のほうが何かと都合が良いのは事実です。

  3. AP 実装を書く人ならどうせサーバ立ててるからドメインくらい持ってるやろという突っ込みはナシでお願いします。それを別にしても Cloudflare Tunnel は事前の準備が結構面倒で、CI などでは使いにくい気がします。今回の方法を確立するまでは ngrok を使っていたので、あまり真面目に検討はしていません。

  4. この環境変数は最近 httptap を見て知りました。

  5. ドメイン名ではなく IP アドレスに対して TLS 証明書を発行することも可能かもしれませんが、Mastodon の Web UI からだと IP アドレスではメンション対象になってくれなかった気がします。試していません。

  6. この環境変数はのえるさんに教えてもらいました

  7. LD_PRELOAD を活用することで通信を全て SOCKS 経由にすることができるツールとして一番有名なのは socksify ですが、socksify には「直接通信できるエンドポイントの場合は DNS 解決をクライアント側で行う」という機能がついているため、ここでは使えませんでした。

  8. kneesocks は io_uring には対応していないっぽいので注意が必要です。Waq は Eio 経由で io_uring を使っていたのですが、そのままだと動かなかったので EIO_BACKEND 環境変数を posix にセットして回避しました。