はじめに
こんばんは。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
処理のフロー
- パケットのパース
- 自分宛か判断
- 直接接続されているnetworkに宛先のcontainerがあるか判定(っというよりネットワークアドレスから判断)
- ARP tableから宛先のMac Addressを取得
- ARP tableに宛先の情報がなかったら、ARPのリクエストを送信、レスポンスが帰ってきたら、ARP tableに追加
- 送信元と宛先のMac Addressの書き換え
- 送信
3の後にrouting table見にいく処理も必要かも。
っというより3をする前にrouting tableを見に行かないといけないのかもしれないが、routing tableができてないので、省略。
routerの実装
main loop
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).
packetの受信を行う。
ETH_P_ALLで自分宛以外のパケットも受信する(自分が送信するパケットも)。
{ok, FD} = procket:open(0, [
{protocol, ?ETH_P_ALL},
{type, raw},
{family, packet}
]),
packetを受け取り、それが自分宛の場合のみ、すぐにプロセスを起動させる。
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
自分宛のパケットかはパケットの宛先が自分に接続されているインターフェイスが宛先になっているかで判断する
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});
ARP Table
arp tableで直接繋がれているcontainerのIPとmac addressとの関連付けを行う。
基本的にpingなどでrouterがパケットを受け取った時にそれが直接繋がれてるネットワークの範囲に含まれているかを判断。
一部抜粋。
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.
interface:searchInterfaceの実装
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.
getInterfaceの実装。
とりあえず"eth"でのインターフェイスのネットワークで検索。
getInterface() ->
{ok, IfLists} = inet:getifaddrs(),
Listen = lists:filter(fun(Elm) -> interfaceList(Elm) end, IfLists),
Listen.
interfaceList(Elm) ->
{Name, _} = Elm,
string:find(Name, "eth") =/= nomatch.
送信先のインターフェイスが決まったのでARPパケットをブロードキャスト。その時のパケット
172.31.0.2 -- > 172.31.0.3 -- > 172.17.0.5
ARP record
% ARP header
-record(arpHeader, {
hardwareType,
protocol,
addressLen,
protocolLen,
operationCode,
sourceMacAddress,
sourceIPAddress,
destMacAddress,
destIPAddress
}).
% ARP table
-record(arpTable, {
sourceIpAddress,
macAddress,
ipAddress,
type
}).
送信
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が決まりました。これで送信します。
% 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.
IP Routing Table(作成中)
直接繋がってないネットワークにパケットを送る時に参照する。
routing table
% 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
}).
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も実装予定。