モチベーション
所要で偽DNSサーバ(上位のコンテンツ(権威)サーバ)を作る必要があり、手っ取り早く使えそうなライブラリということでTwistedを使ってみることにしました。
結構いろいろなところにハマったので、そこらへんをまとめておきます。
ハマったこと色々
ハマったこと1 非同期の考え方
Twistedは非同期の実装になっています。
ある処理をして、結果を受け取って、次の処理をする、という同期的な考え方でロジックを組むと、、実装ができなくなります。
非同期実装はしっかりと学んだことがなく、、ココらへんの理解がまずひと手間でした。
具体的には?
今回の処理上、DNSリクエストを受け付けてから、本来のDNS権威サーバのレコードを取ってくる、ということをする必要があります。
これを何も考えずサンプルソースに手を入れる形で実装すると、、、つまづきます。
私が今回やりたい処理を同期的な考え方で実装すると、以下のようになります。
- DNSリクエストを受け付ける
- DNSリクエストを解釈して、本来の権威サーバにリクエストをかける
- 権威サーバからのレスポンスを受取る
- 受け取った中身を解釈して、リクエストしてきたクライアントに加工したレスポンスを返す
一方、Twistedのサンプルソースでは、以下のような実装になっています。
https://github.com/twisted/twisted/blob/trunk/docs/names/howto/listings/names/override_server.py
- 以下の処理をするクラスを実装して、インスタンスを作成する(DynamicResolver)
- DNSリクエストを解釈して、中身に応じた加工をして返す。
- 条件に合わなければ、failを返す。
- DNSリクエストを受け付けたときの処理に、1をコールバックとして登録する
- コールバックは複数登録していて、1が失敗したら2つ目のResolver(OSのディフォの/etc/hosts、ローカルキャッシュ、スタブリゾルバに問い合わせる)が実行される。
- リクエストを受けたら、1の処理が呼び出され、1の処理の戻り値をクライアントに返す。
- 1の処理がFailしたら、2つ目のResolverが処理して、結果をクライアントに返す。
非同期では、処理はすべて「コールバックとして、別の処理に預けて、別の処理が終わったあとに続けて実施してもらう」というような実装になるため、作りがガラリと変わります。
サンプルソースに対して、同期的な考え方での私の2と3の処理を差し込むには、、、
以下のようなコードを差し込むことを思いつくのですが、そうは動きません。
r = client.Resolver()
response = r.query(<権威への問い合わせ>)
上記は同期的な発想なのですけれど、非同期に動くResolverのqueryは、「その場で処理して戻り値を返してくれる」ということはしてくれません。
コールバックを預けといてDNSの問い合わせが終わり次第それを実行する、というような実装にしなければなりません。
r.queryは結果ではなく、実施予定(実施中)のコールバックのチェーン(defer)を返してくれるので、そこに実行したい続きの処理(3の権威レスポンスの解釈と、4のクライアントへのレス返し)を預ける、というような実装になります。
。。。という話がとんとピンとこず。
非同期+Twistedのお勉強
腰を据えて以下を読み込むことになりました。
https://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/
腰を据えて読むと非常によくわかり、ちゃんと動くコードがかけるように放ったのですが、、、これ変に詩的な表現多用してくれてるんで読みにくいこと読みにくいこと。。
日本語訳もおかしなことになってる箇所が多々あるし。
結構しんどかったです。
ハマったこと2 結構古いコード
総じて、Twistedのコードって結構古いんですよね。。
どこらへんが古いかというと、以下。
Aptで古いバージョンが入ってしまってる
これはTwitedが悪いわけではないのですが。
エラー時のスタックで表示される行の内容がどうもGitで見てるコード合わないないなと調べていて判明。
PIPで最新版を落としてきていたのですが、実はOS(Ubuntu)のパッケージでpython3-twistedというのが入ってしまっていて、そっちが優先されてしまってたんですね。。。
OSのディストリ管理のパッケージの宿命といいますか、めっちゃ古い。
ubuntu20.04だと18.09。21.10だと20.3.0。
現時点の最新版は21.7.0。
dpkg側を削除して、ようやく最新版で動くようになりました。
古いから悪いというか、OSのディストリに取り込まれるくらい古くて実績があるというべきか。
いずれにせよ、注意が必要です。
マイナー(?)な機能にバグ、、
権威サーバとして使う場合はゾーンファイルを作成しておいてそれを読み込んで使います。
https://github.com/twisted/twisted/blob/trunk/docs/names/howto/names.rst
で、pyzoneという形式で読み込む分にはいいのですが、bindzone形式で読む場合(--bindzone)というのを使うとエラーで止まります。
コードを見てみると、以下の記載があるのですが、python2の場合前者と後者がどちらもtype 'str'として解釈されて結合できるのですが、python3の場合前者がclass 'str'、後者がclass 'bytes'として扱われるので、結合ができずきエラーが出る、と。
self.origin = nativeString(fp.basename() + b".")
ここだけ直してもまた別の箇所でエラーを履いてしまうので、結構手を入れないといけなさそうです。
テストファイルもなさそうなので、メンテされてないのかもしれませんね。。
Twistedはかなりの数のプロトコルに対応している関係で、マイナーどころの扱いは微妙なのかもしれません。
ハマった、、ほどではないですが、、 権威サーバとしての振る舞い
今回の特殊動作を実装するにあたり、権威サーバ用のFileAuthorityをベースに手を入れるかなと思って動作をみていたのですが。
権威サーバ、特に上位サーバの場合、自分の直接持っている子ドメインのリクエストではなく、孫ドメインのリクエストが来た際に、子ドメインのNSをAuthority応答として返すということをするのですが、この動作を実装していない感じです。
自分が持ってないレコードが来た場合にSOAを答えるというのはやってくれるのですが。
RFCを読み込んではいないのですが、実際にルートやjpなどの権威サーバに問い合わせるとそう動くので、そういうRFCかディファクトがあるのかなと。
まとめ
一応意図したものが動くまでは作り込めたのですが、別のライブラリ探すかなぁ、、
とりあえずこんなところで。