Fusic Tech Blog

Fusicエンジニアによる技術ブログ

開発合宿でルータを作った
2024/03/29

開発合宿でルータを作った

どうもこんにちは。小原です。 以前、会社で開発合宿が行われた際に今まで作っていたソフトウェアルータの機能追加をしました。もともとネットワークが好きだったのとルータをずっと作ってみたいと思っていました。何度か挑戦し、挫折しを繰り返してたんですがやっと形になってきました。 その時に作ったものでARP部分も軽く整備したので実装内容についてまとめたいと思います。

ARPとは

そもそもARPとは?っという人のために。 ARPは簡単にいうとIPアドレスからMacアドレスを取得してくれるものです。ARPはレイヤ2層で動作しており、 rawソケットを使わないとARPパケットを扱うことができません。なのでErlangのprocketっというライブラリを利用しました。

ARPパケットのフォーマット

では簡単にARPパケットのフォーマットを見てみたいと思います。

  • Hardware Type : どんなネットワークを使っているかの情報が入っています。基本的にイーサネットであり、1が入ります。
  • Protocol Type : ネットワーク層のプロトコルを何を使っているかが入っています。IPでは0x0800になります。
  • Hardware length : Macアドレスの長さになります。6が固定で入っています。
  • Protocol Length : IPアドレスの長さになります。IPv4では4が入ります。
  • Operation : ARPリクエストのオペレーション情報が入ります。1がARPリクエストで2がARPリプライになります。
  • Source H/W Address : 送信元のMACアドレスになります。
  • Source Protocol Address : 送信元のIPアドレスになります。
  • Dest H/W Address : 送信先のMacアドレスになります。リクエストの時はFF-FF-FF-FF-FF-FFが入っています。
  • Dest Protocol Address : 送信先のIPアドレスになります

以上がARPパケットのフォーマットになります。 では実際の実装をみていきます。

実際の実装

全体的な流れ

最初に全体的なコードの流れをみていきたいと思います。

パケットを取得したらまずはレイヤ2層からの処理を行っていきます。その次にレイヤ3層の処理と進んでいきます。そのあとは送信先となるIPアドレスからネクストホップのIPアドレスの取得、Macアドレスの取得を行い送信先となるネクストホップを取得します。今回はパケットの取得部分とMacアドレスの取得部分をみていきたいと思います。

パケットの取得

まずは起点となる、パケットの取得部分をみていきたいと思います。

loop(FD) ->
  case procket:recv(FD, 8192) of
    {error, eagain} ->
      false;
    {error, _} ->
      false;
    {ok, Buf} ->
      gen_event:notify(receiver, Buf),
      true;
    _ ->
      true
  end,
  loop(FD).

procketでパケットを受け付けるまで、待機します。パケットを取得後はgen_eventで通知するようにしました。これでパケットを取得した後に複数のプロセスに通知できるようにしています。

% handle event
handle_event(Buf, Fd) ->
  case brook_ethernet:receive_packet(Buf) of
    {ok, _, {data, Data}, {opt, Opt}} ->
      brook_sender:send_packet(ip_request, {Data, Opt});
    {ok, _, _} = Ok ->
      Ok;
    {error, _, _} = Err ->
      Err;
    Res ->
      Res
  end,
  {ok, Fd}.

パケットを取得後は、取得したパケットのチェックサムでパケットが崩れてないか、TTLが0になっていないかなどをチェックをします。(レイヤ3層の処理なんで今回は省きます) そのあとはIPアドレスとルーティングテーブルから、ネクストホップのIPを取得します(「ルーティングテーブルから次に送信するIPが取得できるか」の部分)。 ここで取得したネクストホップのIPアドレスからMacアドレスを取得するようになります。

ではその次の処理をみていきたいと思います。

ARPテーブルから送信先のMacアドレスがあるか確認

send_packet(Data, #{if_name:=IfName, next_ip:=NextIp}=Opt) ->
  case brook_arp:get_mac_addr({IfName, NextIp}) of
    undefined ->
      brook_arp:request_arp(IfName, NextIp),
      brook_arp_pooling:save_pooling(Data, IfName, NextIp),
      {ok, undefined_arp_table, {{next_ip, NextIp}, {if_name, IfName}}};
    DestMac when is_tuple(DestMac) ->
      {ok, ethernet_send_packet, {data, Data}, {opt, Opt#{dest_mac=>tuple_to_list(DestMac)}}};
    DestMac when is_list(DestMac) ->
      {ok, ethernet_send_packet, {data, Data}, {opt, Opt#{dest_mac=>DestMac}}}
  end;
send_packet(Data, Opt) ->
  {error, not_found_if_name_or_next_ip, {opt, Opt}}.

brook_arp:get_mac_addr関数でARPテーブルにIPアドレスと紐づくMacアドレスがあるかどうかチェックをします。もしある場合は送信先Macアドレスに取得したアドレスをセットしてパケットを送信します。もし見つからなかった場合(undefined)の時はARPリクエストを送信します。 brook_arp_pooling:save_pooling に送信するはずのパケットを保存するようにしています。 なぜこのようなことをしているかというと、この段階でARPリクエストを送ったことによりこのパケットは宛先がなく破棄されてしまいます。破棄されていいようなパケットならいいのですがパケットロスを減らすようにしたいので後から送信できるように保存し、後からこのパケットを送れるように保存しています。

ARPリクエストの送信

request_arp(If, Nexthop) ->
  case brook_interface:match({'_', If, '$1', '_', '$2', '_'}) of
    [] ->
      false;
    [{_, _, SourceIp, _, SourceMacAddr, _}] ->
      ARPHeader = to_binary(#arp_header{
        hw_type=?ETHERNET, protocol=16#0800, address_len=16#06, protocol_len=16#04,
        operation_code=16#0001,
        source_mac_addr=tuple_to_list(SourceMacAddr), source_ip_addr=tuple_to_list(SourceIp),
        dest_mac_addr=[16#00, 16#00, 16#00, 16#00, 16#00, 16#00],
        dest_ip_addr=tuple_to_list(Nexthop)
      }),
      brook_sender:send_packet(arp_request, {ARPHeader, If})
  end.

実際の送信処理となります。まずは送信先となるIPアドレスを基にどのネットワークに対して送信するかがわかるのでそこから送信元となるインターフェイスのMacアドレス(作成しているルータのMacアドレス)を取得します。 取得できたらレイヤ2層の送信元Macアドレスに取得したアドレスをセットし、パケットを送信します。

ARPリプライの処理

ではARPリクエストを送れたのでARPリプライが返って来た後の処理をみてみたいと思います。 パケットを取得するところは上記のところと同じで、パケットを取得した後にそのパケットがARPパケットなのかチェックをします。

packet(<<?ETHERNET:16, _:16, _, _, ?OPTION_RESPONSE:16,
          SourceMacAddr:48, SourceIp:32,
          _:48, DestIp:32, _/bitstring>>) ->
  <<S1, S2, S3, S4>> = <<SourceIp:32>>,
  <<D1, D2, D3, D4>> = <<DestIp:32>>,
  <<SM1, SM2, SM3, SM4, SM5, SM6>> = <<SourceMacAddr:48>>,
  ArpTable = #arp_table{source_ip_addr={D1, D2, D3, D4},
      dest_ip_addr={S1, S2, S3, S4},
      dest_mac_addr={SM1, SM2, SM3, SM4, SM5, SM6},
      type=?ETHERNET
  },
  brook_arp_table:save_arp_table(ArpTable);

そして、ARPパケットの場合はARPリプライなのかどうかチェックをし、ARPリプライの場合はARPテーブルにデータを追加します。 これでARPテーブルを更新し、IPアドレスとMacアドレスの紐付けを登録することができました。

終わりに

今回は簡単にARPの実装を紹介しました。仕組み自体はシンプルなので実装自体に手間はかかりませんでした。 リプライが来たあとはテーブルにデータを追加して終わりなので、シンプルな実装です。まだARPのリクエストとリプライの部分しか実装ができていないので、どこかで他のオペレーション処理を実装できればと思います。

kobaru

kobaru

インフラ好きっ子