erlangでrouterを作った（作ってる）

# はじめに
こんばんは。erlangでrouterを作ってる時にきになったとこ、erlangを書いて気になったとこを書いていきます。プログラムっていうよりネットワークの話メインになりそう。プログラムはほとんど見せれる状態ではないので、ほとんど載せません。っというか、erlangはじめて書いたんで、、ソース汚い、、

## 技術選定

自分「goでrouter作ってみようと思うんですよねぇ」
同僚「erlangがいいらしいよ」
自分「え、まじっすか」
同僚「ネットワーク系ならerlangがいいよ」
自分「よし、erlangで書こ」

## 実装環境

* docker network × 3 (network_01, network_02, network_3）
* docker container × 5 (network_01に1台、network_01とnetwork_02の間に1台、network_02に1台、network_02とnetwork_03の間に1台、network_03に1台）
* containerは全てdebian 8

## 実装するにあたって

routerはネットワークでのL3の話。ただL3だけでなくL2も必要なんでraw socketが必要になります。ただerlangではsystem callを呼べないので、今回はprocketっというモジュールを使って実装しました。
そして、、今回、routerを作ってたんですが、、routing　tableの実装が、まだできておらず、っとうか実装方法に迷っていて、、routing tableのプロセスにroutingしたいIPを投げてはいるんですが、そのままIPを返してます。（今後実装予定）

procket module
https://github.com/msantos/procket

コンパイルはrebar3が必要なんでインストールしておきます。

rebar3
https://github.com/erlang/rebar3

## 処理のフロー

1. パケットのパース
2. 自分宛か判断
3. 直接接続されているnetworkに宛先のcontainerがあるか判定（っというよりネットワークアドレスから判断）
4. ARP tableから宛先のMac Addressを取得
5. ARP tableに宛先の情報がなかったら、ARPのリクエストを送信、レスポンスが帰ってきたら、ARP tableに追加
6. 送信元と宛先のMac Addressの書き換え
7. 送信

3の後にrouting　table見にいく処理も必要かも。
っというより3をする前にrouting tableを見に行かないといけないのかもしれないが、routing tableができてないので、省略。

## routerの実装

### main loop
<pre>
main() ->
    ListenInterfaces = interface:getInterface(),
    {ok, FD} = procket:open(0, [
            {protocol, ?ETH_P_ALL},
            {type, raw},
            {family, packet}
        ]),
    % arp main proccess
    ArpProc = spawn(arp, main, [FD, [], ListenInterfaces]),
    RoutingTableProc = spawn(routingTable, main, [[]]),
    loop(FD, ArpProc, RoutingTableProc).

loop(FD, ArpProc, RoutingTableProc) ->
    {Result, Buf} = procket:recvfrom(FD, 4096),
    if
        Result == error ->
            false;
        Result == ok ->
            {Ethernet, IPLayer, _} = parseBuf(Buf),
            if
                IPLayer == false ->
                    true;
                true ->
                    % search receive interface
                    % whether the recieved packet is itself or not
                    Dest = lists:filter(fun(Elm) -> interface:searchInterface({ethernet, Elm, Ethernet}) end, interface:getInterface()),
                    if
                        length(Dest) == 1 ->
                            _ = spawn(main, followPacket, [ArpProc, RoutingTableProc, Ethernet, IPLayer]);
                        true ->
                            true
                    end
            end;
        true ->
            true
    end,
    loop(FD, ArpProc, RoutingTableProc).
</pre>

packetの受信を行う。
ETH_P_ALLで自分宛以外のパケットも受信する（自分が送信するパケットも）。
<pre>
{ok, FD} = procket:open(0, [
            {protocol, ?ETH_P_ALL},
            {type, raw},
            {family, packet}
        ]),
</pre>

packetを受け取り、それが自分宛の場合のみ、すぐにプロセスを起動させる。
<pre>
Dest = lists:filter(fun(Elm) -> interface:searchInterface({ethernet, Elm, Ethernet}) end, interface:getInterface()),
                    if
                        length(Dest) == 1 ->
                            _ = spawn(main, followPacket, [ArpProc, RoutingTableProc, Ethernet, IPLayer]);
                        true ->
                            true
                    end
</pre>

自分宛のパケットかはパケットの宛先が自分に接続されているインターフェイスが宛先になっているかで判断する
<pre>
searchInterface({macAddress, Interface, DestMacAddress}) ->
    {_, Eopt} = Interface,
    [{flags, _}, {hwaddr, Hwaddr}, {addr, _}, {netmask, _}, {broadaddr, _}] = Eopt,
    if
        Hwaddr =:= DestMacAddress ->
            true;
        true ->
            false
    end;

% search to Ethernet
searchInterface({ethernet, Interface, Ethernet}) ->
    searchInterface({macAddress, Interface, Ethernet#ethernetHeader.destMacAddress});
</pre>



### ARP Table
arp tableで直接繋がれているcontainerのIPとmac addressとの関連付けを行う。
基本的にpingなどでrouterがパケットを受け取った時にそれが直接繋がれてるネットワークの範囲に含まれているかを判断。
一部抜粋。
<pre>
Source = lists:filter(fun(Elm) -> interface:searchInterface({ipNetMaskAddress, Elm, DestIPAddress}) end, interface:getInterface()),
if
    length(Source) == 1 ->
        lists:foreach(fun(Elm) -> sendMessageInterface(Elm, ARPTableRecord, IPLayer) end, Source),
        true;
    true ->
        false
end.
</pre>
interface:searchInterfaceの実装
<pre>
searchInterface({ipNetMaskAddress, Interface, DestIpAddress}) ->
    {_, Eopt} = Interface,
    [{flags, _}, {hwaddr, _}, {addr, Addr}, {netmask, NetMask}, {broadaddr, _}] = Eopt,
    NetMaskAddress = lists:zip3(tuple_to_list(Addr), tuple_to_list(NetMask), DestIpAddress),
    % ネットワークアドレスとにDestIpAddressがマッチしているか
    Func = fun(Elm) -> matchNetMaskAddress(Elm) end,
    IsNetMask = lists:all(Func, NetMaskAddress),
    if
        IsNetMask ->
            true;
        true ->
            false
    end.
</pre>

getInterfaceの実装。
とりあえず"eth"でのインターフェイスのネットワークで検索。
<pre>
getInterface() ->
    {ok, IfLists} = inet:getifaddrs(),
    Listen = lists:filter(fun(Elm) -> interfaceList(Elm) end, IfLists),
    Listen.

interfaceList(Elm) ->
    {Name, _} = Elm,
    string:find(Name, "eth") =/= nomatch.
</pre>

送信先のインターフェイスが決まったのでARPパケットをブロードキャスト。その時のパケット
172.31.0.2 -- > 172.31.0.3 -- > 172.17.0.5

ARP record
<pre>
% ARP header
-record(arpHeader, {
    hardwareType,
    protocol,
    addressLen,
    protocolLen,
    operationCode,
    sourceMacAddress,
    sourceIPAddress,
    destMacAddress,
    destIPAddress
}).
% ARP table
-record(arpTable, {
    sourceIpAddress,
    macAddress,
    ipAddress,
    type
}).
</pre>

送信

|ARP　packet||
|---|---|
|Harware Type|0x0001|
|Protocol|0x0800|
|hardware len|0x06|
|protocol len|0x04|
|operation|0x0001|
|source harware address(Mac address)|2.66.172.31.0.3|
|source protocol address(IP address)|172.31.0.3|
|dest harware address(Mac address)|0.0.0.0.0.0|
|dest protocol address(IP address)|172.31.0.2|

受信

|ARP　packet||
|---|---|
|Harware Type|0x0001|
|Protocol|0x0800|
|hardware len|0x06|
|protocol len|0x04|
|operation|0x0002|
|source harware address(Mac address)|2.66.172.17.0.7|
|source protocol address(IP address)|172.17.0.7|
|dest harware address(Mac address)|2.66.172.31.0.3|
|dest protocol address(IP address)|172.31.0.3|

これでARP table に Mac address と IP address の関連付けを追加する。
これでethernetのパケットの送信元Mac addressと宛先Mac addressが決まりました。これで送信します。

<pre>
% interfaceからメッセージを送信する
sendMessageInterface(Interface, ARPTableRecord, IPLayer) ->
    % interfaceから名前とオプションを取得する
    {IfName, Eopt} = Interface,
    % オプションから必要な要素を取得
    %     hwaddr : Mac address
    [{flags, _}, {hwaddr, HwAddr}, {addr, _}, {netmask, _}, {broadaddr, _}] = Eopt,
    % ARP tableのレコードから宛先のMac addressを取得する
    DestMacAddress = ARPTableRecord#arpTable.macAddress,
    % Ethernet recordをbinaryに変換 
    Ethernet = ethernet:to_binary(#ethernetHeader{sourceMacAddress=HwAddr, destMacAddress=DestMacAddress , type=?TYPE_IPv4}),

    % socketをinterfaceにバインドする。
    {ok, FD} = procket:open(0, [
            {protocol, ?ETH_P_IP},
            {type, raw},
            {family, packet},
            {interface, IfName}
    ]),
    ok = packet:bind(FD, packet:ifindex(FD,IfName)),
    erlang:open_port({fd, FD, FD}, [binary, stream]),

    IpHeader = IPLayer#ip4Header.binary,

    Buf = << Ethernet/bitstring, IpHeader/bitstring >>,

    % bindしたsocketに対してパケットを送信するようにメッセージを送信
    case procket:sendto(FD, Buf) of
        ok ->
            true;
        {ok, _} ->
            true;
        {_, _} ->
            false
    end.
</pre>

### IP Routing Table（作成中）
直接繋がってないネットワークにパケットを送る時に参照する。
routing table
<pre>
% routing table
-record(routingTable, {
    destination,      % destination ip address
    routeMask,        % route net mask
    nextHop,          % next hop ip address
    interface,        % interface
    metric,           % destination metric
    routeType,        % route type static or dynamic
    sourceOfRoute,    % routing protocol
    routeAge,         % route age count
    routeInformation, % other route information
    mtu               % MTU
}).
</pre>

routing tableから送る時のインターフェイスと宛先のIP addressを取得する。この時にrouteTypeとmetricでrouting tableで保存されているレコードの優先順位で宛先を決める。


## まとめ

erlangがなかなか曲者、、書き方すら知らない状態から書き始めたから意味のわかんないコード、汚いコードが多々ある。。（多々というか、設計思想もなんもない状態なんで、ただただ汚い。リファクタ決定）。
書いていけばerlangの特性が面白い。パターンマッチとか、リスト内包記法とか、、

課題として、どのタイミングで直接接続されているcontainerのARP tableとrouting　tableを更新させるかが悩みどこ。networkに繋いだ瞬間にそのnetworkの全てのcontainerのARP tableとrouting tableを更新するすればいいのだが、、、もう少し、いい実装がないか考え中。

1パケット1プロセスの感覚だった、パースのところからべつプロセスですればよかったかも、

routing tableが途中なのと、static routeしか実装してないんでdynamic routingをさせたい。
とりあえずRIPかな。
あとはBGPも実装したいけど、、ローカルでやるの難しそう、、
そして、firewallも実装予定。

リポジトリ
https://github.com/KobaruTakahiro/erlang-router