「ネットワークをソフトウェアから触る話」とのお題に対して旬な話題の投稿が続いているところで、6日目の今回は古めの小ネタの掘り起こしで恐縮なのですが、さくらインターネットのネットワークエンジニアが1本書いてみました。
こんなとこに触りたかった
通常インターネット上でオリジネートする自ASのプレフィクスは、複数のBGPルータで設定されて適切に管理されているはずです。一方でインターネットとの通信を維持するための経路生成とは異なり、弊社のサービス回線帯域を逼迫するDoS攻撃への対応手段としてRTBHと呼ばれる経路広報を弊社網内で一時的に実施して、攻撃終息後に広報を停止するという作業が、ここ数年とくに頻繁に行われています。RTBHの目的は、通常の経路生成とは逆にインターネットからの流入パケットをドロップすることですが、こちらも単一のルータでのみ設定されている状態だと、経路生成ルータの障害時などにDoS攻撃パケットが再流入してきてしまいます。
とはいえ、いわば「有事」に際して複数のルータの設定をマニュアルで実施するのは煩雑で、また高頻度で繰り返すうちにミスを犯すリスクもあります。さらに5年ほど前から、弊社の顧客が利用するネットワーク内へのDoS攻撃に対して、お客様自身がRTBHを仕掛けられるインターフェイス(telnetによるCLI)を提供することになりました。そうなると、RTBH実施と解除のたびに、お客様に複数のルータを設定する手間を煩わせたくはありません。
というわけで長い前置きでしたが、ひとつのルータのRTBH経路広報と停止の設定を別のルータへレプリケートする仕掛けをつくることになりました。
こんなふうに触ってみた
設定の複製を実行するタイミング
弊社では、RTBH経路生成専用ルータ(トリガールータと呼びます)としてquaggaのbgpdを利用していますが、やりたいことは、2機のPCサーバで稼働中のいずれかのbgpdでRTBH経路が追加・削除されたらもう一方のサーバのbgpdへ設定を複製する、ということになります。
この手の処理を実行するタイミングは、処理を実装する側が制御できるようにしておくべきかと思います。定時に実行するようなやり方は手っ取りばやいかもですが、たまたま不適切なタイミングで実行されてしまうと悲惨なことになりがちです。とはいえ、bgpdの稼働設定(running-config)変更から呼び出して経路の複製処理を実行するとなると、quaggaのソースコードに手を入れることになって、それ自体がけっこう大変な作業になっちゃいますし、また将来的なコード管理の負担も問題となります。
そこで、稼働設定を起動時設定(startup-config)に反映するタイミング、つまりCLIからの write memory
実行時に処理を呼び出すことを考えました。これならquagga/bgpd側の処理には全く依存しません。また、サーバ間での排他制御処理とかしなくても、「ふだん設定を投入するのはこっち側のサーバのbgpd」と決めておけば運用で十分カバーできます。つまり各サーバでローカルファイルシステム上の特定のファイルを監視していればいいわけです。具体的には、以下のフロー図のようなプロセスを2機のサーバで個別に起動しておけばいいことになります。
どうしてGNOMEのAPI?
ファイルシステムのイベントを監視するんだからカーネルのAPIを使って、、というのが素直なアプローチといえるかもしれませんが、LinuxとFreeBSDが平和的に共存してる弊社とかですと、最初からOSアグノスティクなコードを書いておいたほうがいいこともあったりします。
実際にトリガールータのbgpdはFreeBSDのjail環境で複数のプロセス(RTBHを設定するお客様ごとに2機のサーバで1プロセスずつ)を実行しているのですが、次のリプレースのときはLinux、ということになったとしてもそのまま使いまわしてもらえたらラクできてうれしいです。
なので、少しくらいのオーバーヘッドがあってもOSの違いを吸収してくれる中間的なライブラリでよさげなのないかなぁと調べてみたら、GNOMEプロジェクトの開発環境でGIOというファイルシステム操作用のAPIがあり、しかもいろんな言語へのバインディングが使えるようになっていました。ほんとにありがたいです。
ファイル監視処理の書き方
さっそくPythonモジュール(PyGObject)をいれてためしたところ、十分に軽量に動作する監視ループをとても簡潔に記述できたので、使わせていただくことにしました。ただ、当時のpygobjectのバージョンは[2.x]
(https://developer.gnome.org/pygobject/stable/pygobject-introduction.html)だったのですが、現在は[3.x](https://wiki.gnome.org/action/show/Projects/PyGObject?action=show&redirect=PyGObject)になっていました。なので、いちおう動作確認できた3.xの作法で記述する例を紹介します。Ubuntu 14.04.3 LTSに以下のパッケージをインストールして確認してます。
- python-gi
- gir1.2-gtk-3.0
from gi.repository import Gtk, Gio
# コールバック関数
def callback(m, f, o, e):
if e == Gio.FileMonitorEvent.CREATED:
print "Someone must have entered 'write memory' command!"
"""
ここでレプリケートの処理をゴニョゴニョと、、、
"""
if __name__ == '__main__':
# 監視対象ファイルのchangedイベントにコールバック関数をバインド
gio_file = Gio.File.new_for_path('<bgpd startup-config>')
gio_file_mon = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
gio_file_mon.connect("changed", callback)
# メインループ開始
Gtk.main()
で、「ここにゴニョゴニョ、、、」のコメントのとこで実際にやるのが、上のフロー図の橙色で囲んだ処理です。つまり、ふたつのtelnetセッションを開始して、ふたつのbgpdが生成する経路を比較して、違いがあればリモート側がローカル側と同じになるように追加・削除してから write_memory
してログアウト、という感じです。Pythonでexpectなら定番の pexpect を使ってますが、expect関連のネタは昨日も含めてこのあともたくさん書いていただけるようなので、今回は割愛します。
どうやって触りつづけるか
やりたかったことをしてくれるコードが書けたところで、あとはこれをどう実行するかですが、daemontools がこの手のフォアグラウンドで実行するプログラムを運用で使うのにすごく適していて、個人的には既に15年くらいお世話になってます。ということで、2機のPCサーバにそれぞれ以下を設置して、
- 共通の実行ファイルとインポートするモジュールファイル
- jailで動くbgpdのプロセス数と同じ数(N個)の設定ファイル
- 各bgpdプロセスごとにdaemontoolsでサービスを実行するためのrunスクリプトを置くN個のディレクトリ
サービスをスタートしたらできあがりです。実際に jailed bgpd の1ペア、xyz00000-trigger1とxyz00000-trigger2で同期させてみます。
xyz00000-trigger1へCLIから以下を実行すると、
xyz00000-trigger1# configure terminal
xyz00000-trigger1(config)# router bgp 65534
xyz00000-trigger1(config-router)# network 192.0.2.12/32
xyz00000-trigger1(config-router)# no network 192.0.2.13/32
xyz00000-trigger1(config-router)# end
xyz00000-trigger1# write memory
xyz00000-trigger1側で実行中のプログラムがファイル更新を検知して同期処理が始まります。
2015-12-04 14:55:07.427446500 xyz00000-trigger1 [INFO ] config_write_file completed.
2015-12-04 14:55:08.380629500 xyz00000-trigger1 [INFO ] xyz00000-trigger1 blackholing [{'rm': None, 'p': '192.0.2.12/32'}, {'rm': None, 'p': '192.0.2.14/32'}, {'rm': None, 'p': '192.0.2.15/32'}]
2015-12-04 14:55:09.264997500 xyz00000-trigger1 [INFO ] xyz00000-trigger2 blackholing [{'rm': None, 'p': '192.0.2.13/32'}, {'rm': None, 'p': '192.0.2.14/32'}, {'rm': None, 'p': '192.0.2.15/32'}]
2015-12-04 14:55:09.265316500 xyz00000-trigger1 [INFO ] sync'ing rtbh config with xyz00000-trigger2...
2015-12-04 14:55:10.399357500 xyz00000-trigger1 [INFO ] xyz00000-trigger2 blackholing [{'rm': None, 'p': '192.0.2.12/32'}, {'rm': None, 'p': '192.0.2.14/32'}, {'rm': None, 'p': '192.0.2.15/32'}]
xyz00000-trigger1側で実行中のプログラムがxyz00000-trigger2へのtelnetセッションでwrite memory
を実行したので、今度はxyz00000-trigger2側で実行中のプログラムがファイル更新を検知して同期処理が始まります。ただしこの時点ではすでに同期された状態なのでなにもせず監視ループにもどります。
2015-12-04 14:55:12.295188500 xyz00000-trigger2 [INFO ] config_write_file completed.
2015-12-04 14:55:13.094760500 xyz00000-trigger2 [INFO ] xyz00000-trigger2 blackholing [{'rm': None, 'p': '192.0.2.12/32'}, {'rm': None, 'p': '192.0.2.14/32'}, {'rm': None, 'p': '192.0.2.15/32'}]
2015-12-04 14:55:13.893095500 xyz00000-trigger2 [INFO ] xyz00000-trigger1 blackholing [{'rm': None, 'p': '192.0.2.12/32'}, {'rm': None, 'p': '192.0.2.14/32'}, {'rm': None, 'p': '192.0.2.15/32'}]
2015-12-04 14:55:13.893673500 xyz00000-trigger2 [INFO ] rtbh config in sync with xyz00000-trigger1
いまでも触りつづけてます
こんなプチ自動化を仕掛けた5年前と現在のDoS攻撃の数や規模を比べるとまさに隔世の感で、その後弊社でも攻撃検知やRTBH実行処理も含めて自動化やWebUI化するなどして対抗してますが、RTBH経路を冗長設定するこの処理は、いまも現役で動いてます。はじめの経路設定処理(telnetやvtyshのCLIからでも、あるいはその手前にあるWebUIや自動処理からでも)と、別のトリガールータへの経路複製処理が完全に独立しているので、今後もし経路設定の自動化処理を作り直すようなことがあっても、経路複製処理はそのままでいけるので、これからも末長く働いてもらうことになりそうです。
ちなみに、これまで実際に2機のサーバのいずれかで障害が発生したのは、自分が知る限り一度というか一時期だけで、原因不明のリブートが数回発生したためリプレースしました。
まとめ
今回の記事で特定のツールやコード記法をお勧めする意図は全然ありません。自動化って当然ながらいろんな目線から考えてみることができると思います。たとえば、ワークフローに着目した3日目の記事がたいへん参考になりましたが、それと対照的に、日頃の手動運用のワンステップのなかにも非効率やリスクを認識したら、問題点を整理して解決のためのちょっとした仕掛けを地道につくってみるっていうのも、吉となることがあったりします。
そんなときこそ、「ヨソさまではどんなのふうにやってるんだろう?」ではなく、「ウチのこの問題をこうやって解決するためにこんなことできるツールや手法はないかな、きっとあるはずだよな」と楽観的に調べてみると、「コレって使えるじゃん!」みたいなことが結構あります(もちろんない場合もありますが)。
そして、もし選択肢が複数あったら、
- 実行性能
- 枯れ具合
- 保守性(継続運用の容易さ)
- コンポーネント間の非依存性
といった観点を含めてバランスのとれた手法でまず実践して、もし結果がいまひとつでも改善していけばきっと効果があらわれるはずと信じて、、、ここまで読んでいただいた皆さまがポジティブなNetOpsとして2016年もご活躍されることを期待しています!