メールリレーをNeo4jで可視化する

はじめに

メール関連の仕事をしていると、自分が受け取ったメールがどのサーバ(もしくはサービス)を経由して送られてきたのかがとても気になります。新しいメールが届く度、ヘッダを見ては「あーSESかー」とか「へーMandrillかー」とか「おっSendGrid!」といった感じで本文よりよっぽど楽しい場合もあったりします。でも、毎回生のヘッダを見るのはいい加減面倒になってきたので、もうちょっとマシな方法を考えてみました。

メールヘッダ

SMTPというプロトコルはヘッダに経由情報が付加されていくという妙な性質を持っています。こういった振る舞いをするプロトコルって他にあるんですかね?あまり思い当たりません。元々、経由情報は正常にリレーされなかった場合のトラッキングが目的だったりするわけですが、様々な情報が得られるので意外と興味深いものだったりします。

経由情報を可視化する

こうした経由情報ですが、正直見やすいものではありません。

Received: from [127.0.0.1] (localhost [54.64.73.243]) by ismtpd-047 (SG) with ESMTP id 14abd28c69e.67a6.2d8d for @yyyy.jp>; Tue, 06 Jan 2015 02:52:54 +0000 (UTC)

この場合、ismtpd-047というホストがlocalhost [54.64.73.243]というホストからESMTP経由でメッセージを受け取ったよ、ということを表しています。ルールを知っていれば読めるわけですが、実はフォーマットの自由度が高くて、この仕様決めた奴を一発ぶん殴りたくなるくらいフリーダムです。
せっかくこういった情報が公開されているわけなので可視化したら何か見えてこないかなー、と思っていたところ、以前から使ってみたかったNeo4jのことを思いつきました。Neo4jはグラフDBという類のDBで、Webベースの可視化機能も利用できるのでお手軽そうです。

システム構成っぽいやつ

受信したメールのヘッダを解析してNeo4jに突っ込む方法ですが、今回は、メールを受けるのにSendGridのParse Webhookを利用してみました。
普段使っているGmailに届くメールをParse Webhook用のアドレスに全部転送します。こうすると、あらかじめ指定したURLにSendGridがメールの内容をPOSTしてくれるので、これをWeb Appで受け、Receivedヘッダを抽出して解析および付加情報をつけてNeo4jに投入します。
こんな感じ。

データ構成っぽいやつ

データ構成についてまとめます。
まず、メールヘッダのうち、データ化の対象とするのは、以下のヘッダです。
  • Received
  • Date
  • From
  • To
  • Message-ID
Receivedヘッダについては、from、by、for、with、id、Timestampに分離します。
さらに、fromとbyについては、ホスト名またはIPアドレスに分離して、どちらかが取得できたらDNS参照もしくは逆引きをして他方を取得します。
さらにさらに、IPアドレスが取得できていたらIPInfoDBにより位置情報を取得します。
fromとbyをそれぞれNode(グラフDB上の意味で)として扱い、Relationship(グラフDB上の意味で)を張ります。
なお、Nodeはホスト名+IPアドレスで一意とします。
Receivedヘッダが複数あれば、Receivedヘッダの逆順(ヘッダ上では新しいものが上に現れるので)にRelationshipを張っていきます。
Relationshipのプロパティには、データ化対象のDate〜Message-IDヘッダを設定します。
って、字でつらつら書いてもわかりにくいと思うので、アウトプットのところで見れるといいですね。

アウトプット

こんな設定をした上でデータが溜まるのを1ヶ月ほど待ってみました。
いくつか興味深いアウトプットが得られましたので晒してみます。

その1:message_id完全一致検索

message_idは概ねメール毎に一意となることから、同じmessage_idをもつパスを検索してみます。一通のメールがリレーされた経路が出てきます。北斗神拳は一子相伝、のパターンですね。
紫色のLocalNodeはIP Geolocationが取得できなかったNode(つまり、内部ネットワークに隠されている可能性が高い)。
オレンジ色のGlobalNodeはIP Geolocationが取得できたNode(つまり、インターネット上に公開されている可能性が高い)です。
GlobalNodeとLocalNodeを経由してメールがリレーされている様子が伺えます。



その2:message_idのドメイン部分の部分一致検索

次にmessage_id内のドメイン部分に部分一致する条件で検索してみます。先ほどのパスと平行してたくさんパスが出てきました。
左下の「Tokyo」となっている2つのNodeが送信元です。
そのすぐ右上の紫のNode(...google.co...)と見えるものはGmailのホストです。
本来のメールリレーはここまでです。その右上のオレンジいっぱいと紫いっぱいのNodeは何かというと、一旦GmailからSendGridのParse Webhook向けに転送された際のリレーです。
次のようなことがわかります。
  1. Gmailはメールを転送する際、アウトバウンドのホストをNew YorkとかCaliforniaあたりにたくさん持っている。
  2. メールはSendGridのParse Webhook用のアドレスに転送される。SendGridも受け側のホストをたくさん持っている。



その3:あるホストを起点としたパスの検索

GitHubでフォローしているリポジトリに変更があった際に送られてくる通知メールを見てみます。GitHubのホストを起点(図の左上)としたパスを検索してみました。真ん中辺りのismtpd-???という紫のNodeはたぶんSendGridのWeb APIの受け口のホストです。
たくさんのホストを運用しているのがわかります。
その下のオレンジはSendGridのアウトバウンドのメールサーバのようです。ここでもたくさんホストが運用されています。
一番下は宛先となるGmailのホストです。


その4:fromを指定した検索A

ingressでポータルが攻撃を受けた際に飛んでくる通知メールを見てみました。fromを指定して検索します。
中央のオレンジ(Googleのホスト)から外側の紫(Parse Webhook)に向かって線が出ています。つまり、本来のメール転送のためのリレーが見当たりません。どうも、ingressの通知メールはReceivedヘッダが極端に少ないようです。Google内部の通知なのが理由かもしれません。


その5:fromを指定した検索B

もう一つfromを指定して検索してみます。Amazonさんから送られてくるニュースレター系メールです。一番上のオレンジのNodeはSESです。ワシントンから来るんですね。そこから直接Gmailに届いています。下半分はParse Webhookの転送によるリレーです。なんか普通ですね。



その6:fromを指定した検索C

次はDoorkeeperさんです。なんだかGmailに届くまでに3段階リレーしていますね。理由はよくわかりませんが。

その7:fromを指定した検索D

Money Forwardさんです。左下のLocalNodeからたくさんのGlobalNode(全部Tokyo)を経由してGmailに届いています。同じ役割を持ったサーバが2〜3個ずつ冗長化されている様子が伺えます。リレーが多段になっているのはなんでしょうね?


その8:全Node

最後に全Nodeを表示してみます。中心が宛先となるGmailです。数が多くて画面外にはみ出しちゃってもはや何が何だかわかりませんが、なんかカッコいいです。


まとめ

なんだかよくわからない部分もありましたが、とりあえず楽しかったです。
位置情報がとれているので地図上にNodeをプロットしてみると地域性がわかるかなーと思っていたのですが、パッと見る限り、ほとんどのNodeはTokyoとUSなんですよね。あまり面白くない結果になりそうだったのでやめときました。
あと、今回は迷惑メールは対象外にしています。迷惑メールだとまた状況は異なるかもしれません。やる気がでたらチャレンジしてみたいと思います。
今回作ったソースコードはこちらにあげときます。


コメント

このブログの人気の投稿

Execノードを使う

Joinノードを使う(その4)

SendGridのX-SMTPAPIヘッダの使い方(Section Tags、Substitution Tags編)