Skip to content

Latest commit

 

History

History
259 lines (182 loc) · 19.9 KB

router_part2.adoc

File metadata and controls

259 lines (182 loc) · 19.9 KB

ルータ (後編)

ルータが持つ重要な機能であるルーティングテーブルの詳細を見ていきましょう。ルータは実に巧妙な仕組みで転送先の情報を管理します。

map

宛先ホストをまとめる

ルータが管理するルーティングテーブルは、宛先ホストが増えるごとに大きくなります。前編の説明では、ルータは宛先ホスト1つごとにルーティングテーブルのエントリを管理していました。しかしこれでは、たとえばインターネットにホストが加わるごとに、インターネット上のルータはルーティングテーブルを更新する必要があります。しかも、インターネット上のホスト数は直線的に増え続け、2016年現在では10億台を超えています。そうなると、宛先ホストごとにエントリを管理する方法は非現実的です。

これを解決するために、ルータは同じイーサネット上にあるホストを1つのグループとしてまとめます。そして、ルーティングテーブルの宛先として、ホストではなくこのグループを指定することで、エントリ数を圧縮します。このとき、グループ情報として使うのがネットワークアドレスとネットマスク長です。

router network2
図 13-1: 同じイーサネット上にあるホストを一つの宛先にまとめる

宛先ホストのグループ化は次のように行います。たとえば、図13-1の右側のネットワークは、ネットワークアドレスが192.168.1.0でネットマスク長が24です(これを192.168.1.0/24と表現します)。このネットワーク中にあるホストX,Y,Zには、ネットワークアドレスと上位24ビットが同じとなるように、つまりIPアドレスが192.168.1で始まるようにします。こうすれば、ホストX,Y,Zは同じ1つのグループ192.168.1.0/24に属するとみなせます。

このようにアドレスを振ることで、ルータAのルーティングテーブルは、図13-1のようにシンプルに書けます。ホストX,Y,Z宛てのパケットを192.168.1.0/24グループ宛てと表現することで、エントリを1つにまとめられるのです。

このとき、ホストX(192.168.1.1)宛のパケットを受け取ったルータAは次のように動作します。ルーティングテーブルのエントリ192.168.1.0/24と、パケットの宛先192.168.1.1との上位24ビットを比較すると一致します。そこで、ルーティングテーブルから次の転送先はルータBだとわかります。ホストY,Z宛も同様に処理できるので、このエントリ1つでホスト3台分の宛先をカバーできます。

宛先ホストがルータと直接つながっているかを調べる

図13-1 では、ルータが宛先ホストに直接接続していない場合について説明しましたが、つながっている/いないはどのように判断するのでしょうか?

ルータは、その判断のために、自身のインタフェースに割り当てられた IP アドレスを使います。インタフェースに割り当てる IP アドレスには、ネットワーク中のホストとネットワークアドレスが同じ IP アドレスを用います。図13-2 で、ルータ B のインタフェースには、ホスト X, Y, Z と同じネットワークアドレスになるよう、例えばアドレス 192.168.1.254 を割り当てます。

router address
図 13-2: ルータのインタフェースには、ネットワーク内のホストとネットワークアドレスが同じとなるように IP アドレスを割り当てる

ここで 図13-2 のルータ B が、ホスト X (192.168.1.1) 宛のパケットを受け取った場合について考えます。ルータ B は、パケットの宛先アドレスを参照し、ネットワークアドレスが同じインタフェースを探します。この例では、192.168.1.254 というアドレスがついたインタフェースが見つかります。あとは、このインタフェースを通じて、ARP リクエストによる MAC アドレス問い合わせを行い、ホスト X 宛にパケットを出力します。

ネットワーク宛てのエントリをまとめる

複数のホスト宛てエントリをまとめて出来たエントリは、さらにまとめられる場合もあります。

aggregation
図 13-3: 複数のネットワークへのルーティング情報をまとめる

例として、図 13-3の3つのネットワークに接続するルータBを考えてみましょう。これら3つのネットワークアドレスは、上位16ビットが172.16.0.0で共通です。ここでルータAから見ると、この3つのネットワークへの次の転送先は、いずれもルータBです。そのため、これら3つのネットワークへのルーティング情報は、172.16.0.0/16宛として1つにまとめられます。

1つの宛先に複数のエントリがマッチする場合

パケットの宛先 IP アドレスに複数のエントリが該当する場合はどうなるでしょうか?図13-4 のルータ A がホスト X (172.16.3.1) にパケットを送る場合について考えてみましょう。ルータ A が持つルーティングテーブルは、ルータ B につながる 3 つのネットワーク宛のエントリはまとめることで、図13-4のように 2 つのエントリにできます。しかし、このようにまとめてしまうと、宛先 172.16.3.1 のパケットは、どちらのエントリにもマッチしてしまいます。ルータは、どちらか正しいエントリを選択しなければいけません。

longest match
図 13-4: マスク長が一番長いエントリを選択する

複数のエントリにマッチする場合には、ネットマスク長が一番長いエントリを選択するというルールがあります。これをロンゲストマッチと呼びます。ロンゲストマッチにより、ルータAは、ホストX宛のパケットをルータCへと転送し、その結果ホストXへとパケットが届きます。

すべての宛先にマッチするデフォルトルート

すべての宛先をまとめたルーティング情報をデフォルトルートと呼び、その宛先を 0.0.0.0/0 と表します。ネットマスク長は、ルーティング情報をまとめるとき、ネットワークアドレスの共通部分の長さを表していました。デフォルトルートでは、まとめられた宛先には共通部分が全くないため、ネットマスク長は 0 となります。

default route
図 13-5: 0.0.0.0/0 は、すべての宛先にマッチする

図13-5のように、インターネットに接続するネットワークでのルーティングテーブルについて考えてみましょう。インターネット上のホスト数は膨大なので、宛先ホストをネットワーク単位にまとめたとしても、数十万エントリを扱う必要があります。しかし、図13-5のようにインターネットへの出口が1か所だけの場合、エントリをデフォルトルート1つにまとめられます。これによって、ルーティングテーブル中のエントリ数を大きく減らせます。

図13-5 のように、インターネットとは別にネットワーク (172.16.3.0/24) があっても、デフォルトルートを使うことに問題はありません。172.16.3.0/24 宛のパケットがルータ A に届いた場合、ルータはロンゲストマッチからルータ C へのエントリを選択します。それ以外のパケットは、デフォルトルートによってルータ B へ転送し、インターネットへと転送します。

RoutingTable のソースコード

パケットを書き換えて転送する(再)

RoutingTable クラスのソースコードを見る前に、パケットの書き換えと転送を行う forward メソッドをもう一度見ていきましょう。前章で説明したこのメソッドが行う 5 つの処理のうち、次の転送先と出力インタフェースを決める方法を見ていきます。

SimpleRouter#forward (lib/simple_router.rb)
link:vendor/router/lib/simple_router.rb[role=include]

宛先アドレス (packet_in.destination_ip_address) に対する次転送先の決定は、resolve_next_hop メソッドで行います。

SimpleRouter#resolve_next_hop (lib/simple_router.rb)
link:vendor/router/lib/simple_router.rb[role=include]

このメソッドでは、まず宛先アドレスと同じネットワークアドレスを持つインタフェースを探します。もし見つかった場合には、次の転送先として宛先アドレスをそのまま返します。見つからなった場合には、ルーティングテーブルから次の転送先を検索します。

その後 forward メソッドへ戻り、決定した次の転送先がルータのインタフェースに接続しているかを判定します。

SimpleRouter#forward (lib/simple_router.rb)
link:vendor/router/lib/simple_router.rb[role=include]

この判定は、次の転送先と同一のネットワークアドレスを持つインタフェースがあるかどうかを調べればわかります。もし、該当するインタフェースがない場合、ルータはそのパケットを転送できないので、パケットを破棄して転送処理を終えます。

ルーティングテーブル (RoutingTable クラス) の実装

次にルーティングテーブルのソースコードを見ていきます。

lib/routing_table.rb
link:vendor/router/lib/routing_table.rb[role=include]

インスタンス変数 @db はルーティングテーブルで、ネットマスク長ごと (0 〜 32) に経路を管理します。経路情報はネットワークアドレスをキーとし、次の転送先 IP アドレスを値とするハッシュテーブルです。

ルーティングテーブルの検索は、lookup メソッドで行います。このメソッドでは、宛先 destination_ip_address に該当する次の転送先 IP アドレスを @db 中から探します。ロンゲストマッチを行うために、ネットマスク長が長い順 (32…0) に @db から次の転送先 IP アドレスを探索します。

コンフィグ

ルータが動作するためには、インタフェースのアドレスとルーティングテーブルの設定が必要です。シンプルルータでは、これらの設定を simple_router.conf に記述します。

simple_router.conf
link:vendor/router/simple_router.conf[role=include]

インタフェースの設定では、そのインタフェースの MAC アドレス (:mac_address)、IP アドレス (:ip_address)、ネットマスク長 (:netmask_length) と、このインタフェースが OpenFlow スイッチのどのポート (:port) に対応しているかを指定します。

ルーティングテーブルの設定では、宛先 (:destination)、ネットマスク長 (:netmask_length) と次の転送先 (:next_hop) を指定します。

実行してみよう

いよいよシンプルルータを動かしてみましょう。いろいろなパケットの送受信を行うために、今回は仮想ホストではなくネットワークネームスペースを使います。今まで使ってきた仮想ホストは簡単なパケット送受信機能とカウンタを備えているので、コントローラの初歩的な導通テストには便利でした。ただしコントローラに ssh や http といった通信を流したりベンチマークを計測する場合など、仮想ホストだけでは機能が不十分な場合もあります。ネットワークネームスペース機能を使えば、ping や iperf といったおなじみのツールをはじめ、任意のアプリケーションを仮想ネットワーク上で動かせます。

sample router network
図 13-6: シンプルルータを動かすための構成

たとえば図 13-6 のような IP アドレスとデフォルトルートを持つホスト 2 台をスイッチに接続するには、次のように設定ファイルで vhost の代わりに netns を指定することで、独立した仮想的なネットワーク環境であるネットワークネームスペースを作れます。それぞれの netns のルーティング情報は route で指定できます。

trema.conf
link:vendor/router/trema.conf[role=include]

この設定ファイルを指定し trema runlib/simple_router.rb を実行すれば、図 13-6のネットワーク環境でシンプルルータが起動します。

$ ./bin/trema run ./lib/simple-router.rb -c ./trema.conf

ネットワークネームスペース内で任意のコマンドを起動するためには、trema netns コマンドを使います。たとえば、次のコマンドを実行すると host1 の環境上でシェルを起動できます。

$ ./bin/trema netns host1

起動したシェル上で、ためしに host1 環境のルーティングテーブルを確認してみましょう。

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 host1
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 host1

たしかに、デフォルトゲートウェイは設定ファイル通り 192.168.1.1 となっています。このように、trema netns で起動したシェル上では通常のコマンドを指定した環境上で実行できます。シェルを終了するには exit または kbd:[Ctrl + d] です。

ping で動作を確認する

最初は簡単に ping を使ってシンプルルータが正しく動作しているかを順に確認して行きましょう。まずは、シンプルルータが ping に応答するかどうかの確認です。host1 にログインし、次のようにシンプルルータの IP アドレス 192.168.1.1 に ping を打ってみます。

$ ./bin/trema netns host1
$ ping 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=128 time=47.4 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=128 time=15.0 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=128 time=15.0 ms
64 bytes from 192.168.1.1: icmp_seq=4 ttl=128 time=19.3 ms
64 bytes from 192.168.1.1: icmp_seq=5 ttl=128 time=14.8 ms
64 bytes from 192.168.1.1: icmp_seq=6 ttl=128 time=14.4 ms
64 bytes from 192.168.1.1: icmp_seq=7 ttl=128 time=15.1 ms
^C
--- 192.168.1.1 ping statistics ---
7 packets transmitted, 7 received, 0% packet loss, time 6008ms
rtt min/avg/max/mdev = 14.425/20.189/47.473/11.245 ms

ちゃんと ping が返ってきました。次に、シンプルルータをまたいだ二つのホスト間で通信できることも確認してみましょう。host2 の IP アドレス 192.168.2.2 に対して、host1 から ping を送ります。

$ ping 192.168.2.2
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
64 bytes from 192.168.2.2: icmp_seq=1 ttl=64 time=75.5 ms
64 bytes from 192.168.2.2: icmp_seq=2 ttl=64 time=82.3 ms
64 bytes from 192.168.2.2: icmp_seq=3 ttl=64 time=101 ms
64 bytes from 192.168.2.2: icmp_seq=4 ttl=64 time=83.3 ms
64 bytes from 192.168.2.2: icmp_seq=5 ttl=64 time=78.2 ms
64 bytes from 192.168.2.2: icmp_seq=6 ttl=64 time=76.4 ms
64 bytes from 192.168.2.2: icmp_seq=7 ttl=64 time=70.9 ms
^C
--- 192.168.2.2 ping statistics ---
7 packets transmitted, 7 received, 0% packet loss, time 6008ms
rtt min/avg/max/mdev = 70.995/81.159/101.180/9.050 ms

この場合もちゃんと ping が返ってきています。以上より、シンプルルータのパケット転送を確認できました。

iperf で動作を確認する

次は iperf でネットワークスループットを計測してみましょう。まずは iperf をインストールします。

$ sudo apt-get update
$ sudo apt-get install iperf

iperf はサーバ・クライアント型のアプリケーションなので、まずは host2 上で次のように iperf のサーバを起動しておきます。

$ ./bin/trema netns host2
$ iperf -s --bind 192.168.2.2
------------------------------------------------------------
Server listening on TCP port 5001
Binding to local address 192.168.2.2
TCP window size: 85.3 KByte (default)
------------------------------------------------------------

host1 上では次のように iperf のクライアントを起動し、ベンチマークを実行します。

$ ./bin/trema netns host1
$ iperf -c 192.168.2.2 --bind 192.168.1.2
------------------------------------------------------------
Client connecting to 192.168.2.2, TCP port 5001
Binding to local address 192.168.1.2
TCP window size: 85.0 KByte (default)
------------------------------------------------------------
[  3] local 192.168.1.2 port 5001 connected with 192.168.2.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-16.4 sec   256 KBytes   128 Kbits/sec

仮想環境上なのでこの数字にはほとんど意味はありませんが、ネットワークネームスペースを使えばこのように iperf や ssh, httpd といったサーバ・クライアント型のアプリケーションも実行できます。

まとめ

ルータ編のまとめとして、もっとも重要な機能であるルーティングテーブルを詳しく説明しました。

  • ルーティングテーブルの複数のエントリ(宛先がホストのIPアドレス)を1つのエントリ(宛先がネットワークアドレス)にまとめることで、エントリ数を減らせる

  • こうしてまとめられたエントリは、ネットワークアドレスの一部が同じ他のエントリとまとめることで、さらにエントリ数を減らせる

  • パケットの宛先にマッチするエントリがルーティングテーブルに複数ある場合は、ネットマスクがもっとも長いエントリを優先 (ロンゲストマッチ) する

  • ルーティングテーブルの宛先に0.0.0.0/0を指定することで、パケットがエントリにマッチしなかった場合のデフォルトの宛先、つまりデフォルトルートを設定できる

  • ネットワークネームスペースを使えば、任意のアプリケーションを使ってコントローラをテストできる