Table of Contents
実装したもの
主に二つのものの実装を行いました。
- Firewall
- 障害ツール
Firewallはお馴染みのFirewallの機能で、送信元、送信先のIPとポートから通信を遮断するものです。 障害ツールソフトウェアルータで意図的に障害を起こすものになります。Netflixのカオスモンキーっというツールが意図的に障害を起こすツールでそれを真似てネットワークでの障害ツールを作りたいと思い作りました。 ソフトウェアルータの実装の部分はErlangで実装して拡張部分はElixirで書いています。 そしてソフトウェアルータの部分でパイプライン処理でルーティングの間に別の処理を挟み込めるようにしています。これを利用してFirewallの実装をすすめていきました。
Firewallの実装
ではFirewallの実装を見て行きたいと思います。 まずはFirewallの宣言部分を見て行きたいと思います。
firewall :default do
allow(
source_ip: {192, 168, 20, 0},
source_netmask: {255, 255, 255, 0},
protocol: :ip
)
allow(
source_ip: {192, 168, 40, 0},
source_netmask: {255, 255, 255, 0},
protocol: :tcp
)
deny()
end
宣言自体はマクロで実装しています。
:default
はFirewallのIDのようなものでFirewallを別のものを利用したい時に変更できるようにしました(これをどう使うかはわからないですが)。
allow
マクロで送信元、宛先のIPとポートを宣言します。ここでは送信元のIPのネットワーク帯を宣言しています。
protocol
でプロトコルを決めています。ip
では今の所IP部分のみチェクしています。
tcp
やudp
にするとポートまで見るようにしています。
最後のdeny()
を宣言することで全てのパケットを遮断するようにしています。
deny
マクロでもallow
同様に送信元、宛先のIPとポートを宣言できます。
そして宣言した順番でパケットの評価をして行き、最初にマッチしたものの処理を実行します。最初にallow
にマッチしたらパケットの通過を許可。deny
にマッチした場合はパケットを遮断するようにしています。
マクロ自体の実装はallow
もdeny
もほぼほぼ一緒なのでallow
の実装のみ見て行きましょう。
"""
defmacro allow(c) do
quote do
Eshe.Firewall.__allow__(__MODULE__, Map.new(unquote(c)))
end
end
def __allow__(module, c) do
record = Map.merge(@default_firewall_record, c)
Module.put_attribute(module, :change_firewall_record, {:allow_record, record})
end
defmacro allow(c)
が呼ばれ、すぐに__allow__
関数に処理を渡します。引数のFirewallの条件はキーリストなんであとで使いやすいようにしたいのでマップに変換してます。
@default_firewall_record
と先ほどの条件をマージして定義してない条件をデフォルトの条件を追加しておきます。
Module.put_attribute(module, :change_firewall_record, {:allow_record, record})
で許可するときの処理を:change_firewall_record
に追加します。
change_firewall_record
にはFirewallで宣言した条件を順番に追加して行きます。allow
での宣言なんでallow_record
として追加します。
deny
の場合はdeny_record
として追加するようにしています。
ちなみにdeny()
の実装は以下のようにしています。
defmacro deny() do
quote do
Eshe.Firewall.__deny__(__MODULE__, %{
source_ip: {0, 0, 0, 0},
source_netmask: {0, 0, 0, 0},
dest_ip: {0, 0, 0, 0},
dest_netmask: {0, 0, 0, 0}
})
end
end
ネットマスクを0.0.0.0にしているので全てのパケットにマッチするようにしています。 これで全てのパケットを遮断します。
パケットの遮断
次に実際のパケットの遮断処理を見て行きたいと思います。 まずは核のソフトウェアルータにパイプライン処理を挟み込むところから。
defmacro firewall_through(identifier) do
quote do
identifier = unquote(identifier)
:brook_pipeline.save_before_ip_pipeline(Eshe.Firewall.firewall_filter(identifier))
end
end
def firewall_filter(identifier) do
filter = fetch_filter(Eshe.Supervisor.route_firewall(), identifier)
fn data, option ->
case is_allow_filter(filter, data) do
:ok ->
{:ok, data, option}
error ->
{:error, error}
end
end
end
defp fetch_filter(route_firewall, identifier) do
record = for %{identifier: id, record: record} <- route_firewall, identifier == id, do: record
record
|> List.flatten()
end
firewall_through
マクロを呼び引数にFirewwawllのIDを渡します。ここでは:default
を渡しておきましょう。
:brook_pipeline.save_before_ip_pipeline(Eshe.Firewall.firewall_filter(identifier))
でパイプライン処理を渡します。FirewallはIPパケットを評価する前に行うようにしました。
firewall_filter
関数の返り値で関数を渡します。この返り値の関数内で先ほどのchange_firewall_record
に追加していった条件等を使えるようにします。fetch_filter
の中でFirewallのIDを元にFirewallの条件を取り出します。
条件式
最後にパケットが条件にマッチしているかどうかの確認を行なって行きます。
def is_allow_filter([head | tail], data) do
case record_filter(head, data) do
:ok ->
:ok
:error ->
{:error, :bad_match}
:next ->
is_allow_filter(tail, data)
end
end
is_allow_filter
関数で先ほど追加したFirewallの条件を順番にチェックして行きます。
defp record_filter({:deny_record, record}, data) do
if match(record, data) do
:error
else
:next
end
end
defp record_filter({:allow_record, record}, data) do
if match(record, data) do
:ok
else
:next
end
end
deny
とallow
それぞれの条件でマッチした場合の処理を実装します。
deny
の条件にマッチした場合はerrorを、allow
の条件にマッチしたらokを返します。
マッチしなかった場合はnextを返し、次の条件を評価します。
def match(
%{protocol: protocol} = record,
<<version::size(4), len::size(4), _head::size(88), source_ip::size(32), dest_ip::size(32),
other::binary>>
)
when protocol in [:tcp, :udp] do
{source_port, dest_port} = fetch_port(len, other)
with res <- match_ip([], record[:dest_ip], record[:dest_netmask], dest_ip),
res <- match_ip(res, record[:source_ip], record[:source_netmask], source_ip),
res <- match_port(res, record[:source_port], source_port),
res <- match_port(res, record[:dest_port], dest_port),
res <- Enum.filter(res, &(&1 != nil)),
{:ok, _value} <- Enum.fetch(res, 0) do
Enum.all?(res, fn r -> r == true end)
else
_ ->
false
end
end
ここで条件にマッチしているかどうかをチェックします。
protocol
がtcp
かudp
かどうかチェックをし、tcp
、udp
の場合はポートまで確認します。
第二引数にパケットのデータを渡します。
with
の中で送信元、送信先のIPとポート番号をチェックしていきます。
その結果をres
に保存して行き、Enum.all?(res, fn r -> r == true end)
で全てにマッチしたかどうかを確認します。全てにマッチした場合のみtrue
を返しそれ以外はfalse
を返すので次の条件に進んで行きます。
まとめ
これで一通りFirewallの実装処理が完了しました。送信元と送信先のIPとポート番号パケットを遮断できたのでFirewallとしての機能ははたしているかと。 Firewallの宣言部分の実装をどうするかで迷っていたのですが、EctoのSchemaとかPhoenixのルーティング処理とかのソースを読んで参考にさせてもらいました。 このFirewallの実装を応用して障害ツールの実装したのでまた別の機会に実装した内容と実装方法は書いて行きたいと思います。
kobaru
インフラ好きっ子